refactor: move manifest template resolver into @microsoft/app-manifest and route fx-core through it - #16525
Conversation
…pp-manifest Relocate the DriverContext-free parts of fx-core's manifest templating into @microsoft/app-manifest so the logic can be consumed without fx-core's DriverContext, localization, or FxError: - expandEnvironmentVariable / getEnvironmentVariables (moved from fx-core component/utils/common.ts) - the file() function resolver and file reader (moved from fx-core component/utils/envFunctionUtils.ts), decoupled from DriverContext and raising plain typed errors that carry the offending path/token - the ManifestType enum Also expose expandFileFunctionMacros (the resolution loop, returning the expanded content plus a function count for host telemetry) and resolveManifest, the host-agnostic counterpart of fx-core's getResolvedManifest. Reuses the package's existing strip-bom and fs-extra usage (no new dependencies). Adds unit tests covering file inlining, JSON escaping, BOM/CRLF normalization, nested file() calls, env-as-parameter, ApiSpec skip, and every typed error path.
Now that the resolver lives in @microsoft/app-manifest (reached transitively via @microsoft/teamsfx-api), remove the duplicated logic from fx-core and delegate: - component/utils/common.ts re-exports expandEnvironmentVariable and getEnvironmentVariables from @microsoft/teamsfx-api, so existing call sites are unchanged. - envFunctionUtils.expandVariableWithFunction delegates its loop to expandFileFunctionMacros, keeping only the telemetry event and mapping the resolver's plain errors to localized UserError / fx-core FileNotFoundError via toFxError. getResolvedManifest is unchanged and keeps composing these primitives. Update the affected tests to drive resolution through real temp files instead of fs mocks, since file reads now happen inside app-manifest.
toFxError now returns assembleError(e, source) instead of throwing on the fallback branch, so expandVariableWithFunction always honors its Result<T, FxError> contract. Documents why MissingEnvironmentVariablesError is intentionally unmapped, adds a localization-remap note to ManifestTemplateError, and restores explanatory comments in manifestTemplate.
…fest
getResolvedManifest now delegates its file()+env resolution ordering to @microsoft/app-manifest's resolveManifest (single source of truth), via a new resolveManifestWithContext wrapper that re-adds fx-core's function-count telemetry and localized FxError mapping. resolveManifest returns ResolveManifestResult { content, functionCount } and its MissingEnvironmentVariablesError carries fromPath so the error mapping is uniform in toFxError. A shared runManifestResolver helper removes the duplicated await/try/telemetry shape between expandVariableWithFunction and resolveManifestWithContext. Behavior preserved.
…xtraction # Conflicts: # packages/fx-core/src/component/utils/envFunctionUtils.ts
Possible pre-existing bug: an empty
|
Summary
Builds on #16270 (Ian Clanton-Thuon (@iclanton)). Moves the
DriverContext-free parts of fx-core's manifesttemplating —
$[file('...')]function expansion and${{ENV}}variable expansion — down into@microsoft/app-manifest, so the logic can be consumed without@microsoft/teamsfx-core, aDriverContext, localization, orUserError.This PR goes one step further than #16270: it also routes fx-core's
getResolvedManifestthrough the shared resolver, removing the duplicated orchestration that #16270 intentionally
left in place. This is a behavior-preserving refactor — fx-core keeps only its host concerns
(telemetry + localized error mapping) and delegates the resolution itself to the moved logic.
What moves into
@microsoft/app-manifest(from #16270)expandEnvironmentVariable/getEnvironmentVariables— moved fromfx-core/src/component/utils/common.ts(re-exported there via@microsoft/teamsfx-apisoexisting import paths are unchanged).
file()function resolver and file reader — moved fromfx-core/src/component/utils/envFunctionUtils.ts, decoupled fromDriverContextand raisingplain typed
ManifestTemplateErrorsubclasses that carry the offending path/token.ManifestTypeenum.resolveManifest,expandFileFunctionMacros,processManifestFunction, andthe
ResolveManifestResulttype.What this PR adds on top of #16270
getResolvedManifest(
teamsApp/utils/utils.ts) drops its inlineexpand → env → checkbody and calls the newresolveManifestWithContext, so the whole resolve pipeline lives in one place(
@microsoft/app-manifest). fx-core keeps only the telemetry-property selection it did before.resolveManifestreturns{ content, functionCount }(ResolveManifestResult) instead ofjust a string, so the host can still emit the existing
manifest-with-functiontelemetry eventafter delegating.
toFxErrormaps everyManifestTemplateErrorsubclass toits localized fx-core
UserError/FileNotFoundError, mapsMissingEnvironmentVariablesError(which now carries
fromPath) to fx-core's localizedMissingEnvironmentVariablesError("manifest", names, fromPath), and wraps anything unexpected viaassembleErrorso theResult<string, FxError>contract always holds.runManifestResolverhelper consolidates the try/catch →toFxErrormapping plus thefunction-count telemetry used by both
expandVariableWithFunctionandresolveManifestWithContext.Behavior preservation
ManifestUtils,PluginManifestUtils,CopilotGptManifestUtils,createAppPackage) are unchanged and still receiveResult<string, FxError>.manifest-with-function, customized-keys properties) and localized errormessages/keys are preserved.
Testing
@microsoft/app-manifestunit suite (manifestTemplate.test.ts) coversexpandEnvironmentVariable,getEnvironmentVariables, thefile()resolver,expandFileFunctionMacros, andresolveManifest.manifestUtils.test.tsandenvFunctionUtils.test.tsnow drive$[file()]resolution through real temp files, since the read now happens inside@microsoft/app-manifestand cross-packagefsmocks no longer apply.Notes / follow-ups
@microsoft/app-manifest's published tarballomits
build/json-schemas/, so offline schema validation silently network-falls-back; bundling theschemas would make it fully hermetic.
copilotAgentPluginbuild (ODSP-Web) currently ships an interimin-process copy of this
file()resolver; once this lands and publishes it will swap toexpandFileFunctionMacros/resolveManifest.Opened as a draft (mirrors #16270's draft status) to align with the upstream API-surface review.
Related PRs
file()resolver in the SPFxcopilotAgentPluginbuild; will swap toexpandFileFunctionMacros/resolveManifestonce this lands and publishes.Design / architecture
This PR moves structural shape across a package boundary (host-agnostic manifest-template resolution
fx-core→@microsoft/app-manifest), so it is recorded as an ADR:docs/02-architecture/adr/). Decision: the${{ENV}}/$[file()]grammar lives once in@microsoft/app-manifest; hosts (fx-coretoday, the SPFx build next) wrap it and add only their cross-cutting concerns (telemetry, localizedFxError). Follows the existingfx-core → @microsoft/teamsfx-api → @microsoft/app-manifestdependency direction — it relocates logic to the correct existing layer rather than introducing a new boundary.manifest-schemas.md— the platform manifest schemas the resolved output conforms to (context, not the decision).docs/02-architecture/README.md.