@@ -8,12 +8,18 @@ import {
88 Result ,
99 UserError ,
1010 UserErrorOptions ,
11+ // Host-agnostic manifest-template resolution lives in @microsoft/app-manifest and
12+ // is 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+ expandFileFunctionMacros ,
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,145 +32,64 @@ 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- }
35+ // Re-exported for existing importers (ManifestUtils, PluginManifestUtils, utils, createAppPackage).
36+ export { ManifestType } ;
3637
37- export async function expandVariableWithFunction (
38- content : string ,
39- ctx : DriverContext ,
40- envs : { [ key in string ] : string } | undefined ,
41- isJson : boolean ,
42- manifestType : ManifestType ,
43- fromPath : string
44- ) : Promise < Result < string , FxError > > {
45- const regex = / \$ \[ * [ a - z A - Z ] [ a - z A - Z ] * \( [ ^ \] ] * \) * \] / g;
46- const matches = content . match ( regex ) ;
47-
48- if ( ! matches ) {
49- return ok ( content ) ; // no function
50- }
51- let count = 0 ;
52- for ( const placeholder of matches ) {
53- const processedRes = await processFunction (
54- placeholder . slice ( 2 , - 1 ) . trim ( ) ,
55- ctx ,
56- envs ,
57- fromPath
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" )
5844 ) ;
59- if ( processedRes . isErr ( ) ) {
60- return err ( processedRes . error ) ;
61- }
62- let value = processedRes . value ;
63- if ( isJson && value ) {
64- value = JSON . stringify ( value ) . slice ( 1 , - 1 ) ;
65- }
66- if ( value ) {
67- count += 1 ;
68- content = content . replace ( placeholder , value ) ;
69- }
70- }
71-
72- if ( count > 0 ) {
73- ctx . telemetryReporter . sendTelemetryEvent ( telemetryEvent , {
74- [ TelemetryPropertyKey . manifestType ] : manifestType . toString ( ) ,
75- [ TelemetryPropertyKey . functionCount ] : count . toString ( ) ,
76- } ) ;
45+ return new UnsupportedFileFormatError ( ctx . platform ) ;
7746 }
78- return ok ( content ) ;
79- }
80-
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 ( ")" ) ) {
47+ if ( e instanceof ManifestInvalidFunctionError ) {
8948 ctx . logProvider . error (
90- getLocalizedString ( "core.envFunc.unsupportedFunction.errorLog" , firstTrimmedContent , "file" )
49+ getLocalizedString ( "core.envFunc.unsupportedFunction.errorLog" , e . token , "file" )
9150 ) ;
92- return err ( new InvalidFunctionError ( ctx . platform ) ) ;
51+ return new InvalidFunctionError ( ctx . platform ) ;
9352 }
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
53+ if ( e instanceof ManifestInvalidFunctionParameterError ) {
54+ ctx . logProvider . error (
55+ getLocalizedString ( "core.envFunc.invalidFunctionParameter.errorLog" , e . token , "file" )
10456 ) ;
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
57+ return new InvalidFunctionParameter ( ctx . platform ) ;
58+ }
59+ if ( e instanceof ManifestReadFileError ) {
12460 ctx . logProvider . error (
125- getLocalizedString ( "core.envFunc.invalidFunctionParameter .errorLog" , trimmedParameter , "file" )
61+ getLocalizedString ( "core.envFunc.readFile .errorLog" , e . filePath , e . cause ?. toString ( ) )
12662 ) ;
127- return err ( new InvalidFunctionParameter ( ctx . platform ) ) ;
63+ return new ReadFileError ( ctx . platform , e . filePath ) ;
12864 }
65+ if ( e instanceof ManifestFileNotFoundError ) {
66+ return new FileNotFoundError ( source , e . filePath ) ;
67+ }
68+ throw e ;
12969}
13070
131- async function readFileContent (
132- filePath : string ,
71+ export async function expandVariableWithFunction (
72+ content : string ,
13373 ctx : DriverContext ,
13474 envs : { [ key in string ] : string } | undefined ,
75+ isJson : boolean ,
76+ manifestType : ManifestType ,
13577 fromPath : string
13678) : 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 ) ) ;
79+ let resolved : { content : string ; functionCount : number } ;
80+ try {
81+ resolved = await expandFileFunctionMacros ( content , isJson , { envs, fromPath } ) ;
82+ } catch ( e ) {
83+ return err ( toFxError ( e , ctx ) ) ;
14384 }
14485
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 ) ) ;
86+ if ( resolved . functionCount > 0 ) {
87+ ctx . telemetryReporter . sendTelemetryEvent ( telemetryEvent , {
88+ [ TelemetryPropertyKey . manifestType ] : manifestType . toString ( ) ,
89+ [ TelemetryPropertyKey . functionCount ] : resolved . functionCount . toString ( ) ,
90+ } ) ;
16191 }
162- }
163-
164- function getAbsolutePath ( relativeOrAbsolutePath : string , fromPath : string ) : string {
165- return path . isAbsolute ( relativeOrAbsolutePath )
166- ? relativeOrAbsolutePath
167- : path . join ( path . dirname ( fromPath ) , relativeOrAbsolutePath ) ;
92+ return ok ( resolved . content ) ;
16893}
16994
17095class UnsupportedFileFormatError extends UserError {
0 commit comments