11/**
2- * Expands local composite functions (`uses: ./path/to/function`) into a
3- * {@link CompositeBuildStep} tree at parse time.
2+ * Expands local functions (`uses: ./path/to/function`) into build steps at parse time.
43 *
5- * Each call becomes a node with prefixed child ids (`caller__inner`); the parser
6- * flattens the tree into the workflow. Expanded steps carry a
7- * {@link BuildStepCompositeFunctionScope} so `${{ steps.* }}` and `${{ inputs.* }}`
8- * resolve against composite-function-local names.
4+ * A composite function call becomes a {@link CompositeBuildStep} node with prefixed child ids
5+ * (`caller__inner`); the parser flattens the tree into the workflow. Expanded steps carry a
6+ * {@link BuildStepCompositeFunctionScope} so `${{ steps.* }}` and `${{ inputs.* }}` resolve
7+ * against composite-function-local names. A single-step (`command`/`path`) function call becomes
8+ * one ordinary build step keeping the caller's own id.
99 */
1010import {
1111 CompositeFunctionCatalog ,
1212 CompositeFunctionConfig ,
1313 FunctionStep ,
14+ LegacyFunctionConfig ,
15+ LocalFunctionConfig ,
1416 ShellStep ,
1517 Step ,
18+ isLegacyFunctionConfig ,
1619 isStepFunctionStep ,
1720 isStepShellStep ,
1821} from '@expo/eas-build-job' ;
1922
20- import { BuildFunctionById } from './BuildFunction' ;
23+ import { BuildFunction , BuildFunctionById } from './BuildFunction' ;
2124import { BuildFunctionGroupById } from './BuildFunctionGroup' ;
2225import { BuildStep , BuildStepOutputAccessor } from './BuildStep' ;
2326import { BuildStepCompositeFunctionScope } from './BuildStepCompositeFunctionScope' ;
@@ -31,6 +34,7 @@ import {
3134import { CompositeBuildStep } from './CompositeBuildStep' ;
3235import { BuildConfigError } from './errors' ;
3336import { duplicates } from './utils/expodash/duplicates' ;
37+ import { createBuildFunctionFromLegacyFunctionConfig } from './utils/legacyFunction' ;
3438import {
3539 getLocalCompositeFunctionCallWorkingDirectoryError ,
3640 isLocalCompositeFunctionPath ,
@@ -45,12 +49,16 @@ export type FunctionMaps = {
4549 buildFunctionGroupById : BuildFunctionGroupById ;
4650} ;
4751
48- type CompositeFunctionCall = {
52+ export type FunctionMapsWithExpander = FunctionMaps & {
53+ compositeFunctionExpander : CompositeFunctionExpander ;
54+ } ;
55+
56+ type LocalFunctionCall = {
4957 compositeFunctionPath : string ;
50- /** Caller-assigned id used as prefix for all inner step ids. */
58+ /** Caller-assigned id; for a composite function also the prefix of all inner step ids. */
5159 syntheticStepId : string ;
5260 name ?: string ;
53- /** Caller-provided input values, consumed when composite function inputs are interpolated. */
61+ /** Caller-provided input values, consumed when function inputs are interpolated. */
5462 callWith ?: Record < string , unknown > ;
5563 callIf ?: string ;
5664 parentScope ?: BuildStepCompositeFunctionScope ;
@@ -64,6 +72,9 @@ type StepOverrides = {
6472} ;
6573
6674export class CompositeFunctionExpander {
75+ /** Cached per path: a `BuildFunction` is stateless, and one path may be called many times. */
76+ private readonly legacyFunctionByPath = new Map < string , BuildFunction > ( ) ;
77+
6778 constructor (
6879 private readonly ctx : BuildStepGlobalContext ,
6980 private readonly compositeFunctionCatalog : CompositeFunctionCatalog ,
@@ -78,15 +89,16 @@ export class CompositeFunctionExpander {
7889 return this . functionMaps . buildFunctionGroupById ;
7990 }
8091
81- public expandCompositeFunctionStep (
92+ /** Steps a local function call expands to: many for a composite function, one otherwise. */
93+ public expandLocalFunctionStep (
8294 step : FunctionStep ,
83- compositeFunctionPath : string ,
95+ functionPath : string ,
8496 syntheticStepId : string
85- ) : CompositeBuildStep {
86- this . rejectCompositeFunctionCallWorkingDirectory ( step ) ;
87- return this . expand (
97+ ) : BuildStep [ ] {
98+ this . rejectLocalFunctionCallWorkingDirectory ( step ) ;
99+ const expanded = this . expandCall (
88100 {
89- compositeFunctionPath,
101+ compositeFunctionPath : functionPath ,
90102 syntheticStepId,
91103 name : step . name ,
92104 callWith : step . with ,
@@ -95,21 +107,58 @@ export class CompositeFunctionExpander {
95107 } ,
96108 new Set < string > ( )
97109 ) ;
110+ return expanded instanceof CompositeBuildStep ? expanded . getFlattenedSteps ( ) : [ expanded ] ;
98111 }
99112
100113 // The call step expands away; `working_directory` on it would never apply.
101- private rejectCompositeFunctionCallWorkingDirectory ( step : FunctionStep ) : void {
114+ private rejectLocalFunctionCallWorkingDirectory ( step : FunctionStep ) : void {
102115 if ( step . working_directory !== undefined ) {
103116 throw new BuildConfigError ( getLocalCompositeFunctionCallWorkingDirectoryError ( step . uses ) ) ;
104117 }
105118 }
106119
120+ private expandCall ( call : LocalFunctionCall , visited : ReadonlySet < string > ) : BuildStep {
121+ const localFunction = this . lookupLocalFunction ( call . compositeFunctionPath ) ;
122+ if ( isLegacyFunctionConfig ( localFunction ) ) {
123+ return this . createLegacyFunctionStep ( call , localFunction ) ;
124+ }
125+ return this . expand ( call , localFunction , visited ) ;
126+ }
127+
128+ /**
129+ * A single-step function keeps the caller's id and takes the call-site `if` as its own
130+ * condition; there is no expansion scope to hang it on.
131+ */
132+ private createLegacyFunctionStep (
133+ call : LocalFunctionCall ,
134+ config : LegacyFunctionConfig
135+ ) : BuildStep {
136+ const { compositeFunctionPath } = call ;
137+ let buildFunction = this . legacyFunctionByPath . get ( compositeFunctionPath ) ;
138+ if ( ! buildFunction ) {
139+ buildFunction = createBuildFunctionFromLegacyFunctionConfig ( compositeFunctionPath , config ) ;
140+ this . legacyFunctionByPath . set ( compositeFunctionPath , buildFunction ) ;
141+ }
142+ return buildFunction . createBuildStepFromFunctionCall ( this . ctx , {
143+ id : call . syntheticStepId ,
144+ name : call . name ,
145+ callInputs : call . callWith ,
146+ shell : config . shell ,
147+ env : call . inheritedEnv ,
148+ ifCondition : call . callIf ,
149+ compositeFunctionScope : call . parentScope ,
150+ } ) ;
151+ }
152+
107153 // `visited.size` is the current composite function nesting depth.
108- private expand ( call : CompositeFunctionCall , visited : ReadonlySet < string > ) : CompositeBuildStep {
154+ private expand (
155+ call : LocalFunctionCall ,
156+ compositeFunction : CompositeFunctionConfig ,
157+ visited : ReadonlySet < string >
158+ ) : CompositeBuildStep {
109159 const { compositeFunctionPath, syntheticStepId } = call ;
110160 this . guardAgainstRunawayRecursion ( compositeFunctionPath , visited ) ;
111161
112- const compositeFunction = this . lookupCompositeFunction ( compositeFunctionPath ) ;
113162 const compositeFunctionDisplayName =
114163 call . name ?? compositeFunction . name ?? compositeFunctionPath ;
115164 const innerSteps = compositeFunction . runs . steps ;
@@ -184,14 +233,14 @@ export class CompositeFunctionExpander {
184233 }
185234 }
186235
187- private lookupCompositeFunction ( compositeFunctionPath : string ) : CompositeFunctionConfig {
188- const compositeFunction = this . compositeFunctionCatalog [ compositeFunctionPath ] ;
189- if ( ! compositeFunction ) {
236+ private lookupLocalFunction ( compositeFunctionPath : string ) : LocalFunctionConfig {
237+ const localFunction = this . compositeFunctionCatalog [ compositeFunctionPath ] ;
238+ if ( ! localFunction ) {
190239 throw new BuildConfigError (
191- `Local composite function "${ compositeFunctionPath } " does not exist. Expected a "function.yml" (or "function.yaml") file at "${ compositeFunctionPath } " relative to the EAS project root (convention: ".eas/functions/<name>").`
240+ `Local function "${ compositeFunctionPath } " does not exist. Expected a "function.yml" (or "function.yaml") file at "${ compositeFunctionPath } " relative to the EAS project root (convention: ".eas/functions/<name>").`
192241 ) ;
193242 }
194- return compositeFunction ;
243+ return localFunction ;
195244 }
196245
197246 private expandInnerStep (
@@ -211,8 +260,8 @@ export class CompositeFunctionExpander {
211260
212261 if ( isStepFunctionStep ( innerStep ) ) {
213262 if ( isLocalCompositeFunctionPath ( innerStep . uses ) ) {
214- this . rejectCompositeFunctionCallWorkingDirectory ( innerStep ) ;
215- return this . expandNestedCompositeFunctionCall ( innerStep , {
263+ this . rejectLocalFunctionCallWorkingDirectory ( innerStep ) ;
264+ return this . expandNestedLocalFunctionCall ( innerStep , {
216265 compositeFunctionPath : parseLocalCompositeFunctionPath ( innerStep . uses ) ,
217266 newId,
218267 overrides,
@@ -243,7 +292,7 @@ export class CompositeFunctionExpander {
243292 } ;
244293 }
245294
246- private expandNestedCompositeFunctionCall (
295+ private expandNestedLocalFunctionCall (
247296 innerStep : FunctionStep ,
248297 {
249298 compositeFunctionPath,
@@ -258,8 +307,8 @@ export class CompositeFunctionExpander {
258307 scope : BuildStepCompositeFunctionScope ;
259308 visited : ReadonlySet < string > ;
260309 }
261- ) : CompositeBuildStep {
262- return this . expand (
310+ ) : BuildStep {
311+ return this . expandCall (
263312 {
264313 compositeFunctionPath,
265314 syntheticStepId : newId ,
0 commit comments