1- import { $TSContext } from '@aws-amplify/amplify-cli-core' ;
1+ import { $TSContext , JSONUtilities } from '@aws-amplify/amplify-cli-core' ;
22import execa from 'execa' ;
3+ import * as fs from 'fs-extra' ;
34import { buildCustomResources } from '../../utils/build-custom-resources' ;
45
56jest . mock ( '@aws-amplify/amplify-cli-core' ) ;
@@ -8,15 +9,10 @@ jest.mock('../../utils/dependency-management-utils');
89jest . mock ( '../../utils/generate-cfn-from-cdk' ) ;
910jest . mock ( 'execa' ) ;
1011jest . mock ( 'ora' ) ;
12+ jest . mock ( 'fs-extra' ) ;
1113
12- jest . mock ( 'fs-extra' , ( ) => ( {
13- readFileSync : jest . fn ( ) . mockReturnValue ( 'mockCode' ) ,
14- existsSync : jest . fn ( ) . mockReturnValue ( true ) ,
15- ensureDirSync : jest . fn ( ) . mockReturnValue ( true ) ,
16- ensureDir : jest . fn ( ) ,
17- writeFileSync : jest . fn ( ) . mockReturnValue ( true ) ,
18- writeFile : jest . fn ( ) ,
19- } ) ) ;
14+ const fs_mock = fs as jest . Mocked < typeof fs > ;
15+ const JSONUtilities_mock = JSONUtilities as jest . Mocked < typeof JSONUtilities > ;
2016
2117jest . mock ( 'ora' , ( ) => ( ) => ( {
2218 start : jest . fn ( ) ,
@@ -34,7 +30,15 @@ jest.mock('../../utils/generate-cfn-from-cdk', () => ({
3430} ) ) ;
3531
3632jest . mock ( '@aws-amplify/amplify-cli-core' , ( ) => ( {
37- getPackageManager : jest . fn ( ) . mockResolvedValue ( 'npm' ) ,
33+ getPackageManager : jest . fn ( ) . mockResolvedValue ( {
34+ packageManager : 'npm' ,
35+ executable : 'npm' ,
36+ runner : 'npx' ,
37+ lockFile : 'package-lock.json' ,
38+ displayValue : 'NPM' ,
39+ getRunScriptArgs : jest . fn ( ) ,
40+ getInstallArgs : jest . fn ( ) ,
41+ } ) ,
3842 pathManager : {
3943 getBackendDirPath : jest . fn ( ) . mockReturnValue ( 'mockTargetDir' ) ,
4044 } ,
@@ -44,13 +48,31 @@ jest.mock('@aws-amplify/amplify-cli-core', () => ({
4448 stringify : jest . fn ( ) ,
4549 } ,
4650 skipHooks : jest . fn ( ) . mockReturnValue ( false ) ,
51+ AmplifyError : class AmplifyError extends Error {
52+ constructor ( name : string , options : { message : string } ) {
53+ super ( options . message ) ;
54+ this . name = name ;
55+ }
56+ } ,
4757} ) ) ;
4858
4959describe ( 'build custom resources scenarios' , ( ) => {
5060 let mockContext : $TSContext ;
5161
5262 beforeEach ( ( ) => {
5363 jest . clearAllMocks ( ) ;
64+
65+ // Default fs mocks
66+ ( fs_mock . existsSync as jest . Mock ) . mockReturnValue ( true ) ;
67+ ( fs_mock . readFileSync as jest . Mock ) . mockReturnValue ( 'mockCode' ) ;
68+ ( fs_mock . ensureDirSync as jest . Mock ) . mockReturnValue ( undefined ) ;
69+ ( fs_mock . ensureDir as jest . Mock ) . mockResolvedValue ( undefined ) ;
70+ ( fs_mock . writeFileSync as jest . Mock ) . mockReturnValue ( undefined ) ;
71+ ( fs_mock . writeFile as jest . Mock ) . mockResolvedValue ( undefined ) ;
72+
73+ // Default: no build script in package.json
74+ JSONUtilities_mock . readJson . mockReturnValue ( { } ) ;
75+
5476 mockContext = {
5577 amplify : {
5678 openEditor : jest . fn ( ) ,
@@ -72,10 +94,93 @@ describe('build custom resources scenarios', () => {
7294 } as unknown as $TSContext ;
7395 } ) ;
7496
75- it ( 'build all resources' , async ( ) => {
76- await buildCustomResources ( mockContext ) ;
97+ describe ( 'default behavior (no build script)' , ( ) => {
98+ it ( 'should run install and tsc separately when no build script defined' , async ( ) => {
99+ // No build script in package.json
100+ JSONUtilities_mock . readJson . mockReturnValue ( { } ) ;
101+
102+ await buildCustomResources ( mockContext ) ;
103+
104+ // 2 for npm install and 2 for tsc build (1 per resource)
105+ expect ( execa . sync ) . toBeCalledTimes ( 4 ) ;
106+
107+ // First resource: install then tsc
108+ expect ( execa . sync ) . toHaveBeenNthCalledWith ( 1 , 'npm' , [ 'install' ] , expect . objectContaining ( { stdio : 'pipe' } ) ) ;
109+ expect ( execa . sync ) . toHaveBeenNthCalledWith ( 2 , 'npx' , [ 'tsc' ] , expect . objectContaining ( { stdio : 'pipe' } ) ) ;
110+
111+ // Second resource: install then tsc
112+ expect ( execa . sync ) . toHaveBeenNthCalledWith ( 3 , 'npm' , [ 'install' ] , expect . objectContaining ( { stdio : 'pipe' } ) ) ;
113+ expect ( execa . sync ) . toHaveBeenNthCalledWith ( 4 , 'npx' , [ 'tsc' ] , expect . objectContaining ( { stdio : 'pipe' } ) ) ;
114+ } ) ;
115+
116+ it ( 'should run install and tsc when package.json has scripts but no build script' , async ( ) => {
117+ // package.json with scripts but no build script
118+ JSONUtilities_mock . readJson . mockReturnValue ( {
119+ scripts : {
120+ test : 'jest' ,
121+ lint : 'eslint .' ,
122+ } ,
123+ } ) ;
124+
125+ await buildCustomResources ( mockContext ) ;
126+
127+ // Should still run install + tsc for each resource
128+ expect ( execa . sync ) . toBeCalledTimes ( 4 ) ;
129+ expect ( execa . sync ) . toHaveBeenNthCalledWith ( 1 , 'npm' , [ 'install' ] , expect . objectContaining ( { stdio : 'pipe' } ) ) ;
130+ } ) ;
131+ } ) ;
132+
133+ describe ( 'custom build script behavior' , ( ) => {
134+ it ( 'should run build script when package.json has scripts.build defined' , async ( ) => {
135+ // package.json with build script
136+ JSONUtilities_mock . readJson . mockReturnValue ( {
137+ scripts : {
138+ build : 'pnpm install --ignore-workspace && tsc' ,
139+ } ,
140+ } ) ;
141+
142+ await buildCustomResources ( mockContext ) ;
143+
144+ // Only 2 calls (1 per resource) for 'npm run build'
145+ expect ( execa . sync ) . toBeCalledTimes ( 2 ) ;
146+
147+ expect ( execa . sync ) . toHaveBeenNthCalledWith ( 1 , 'npm' , [ 'run' , 'build' ] , expect . objectContaining ( { stdio : 'pipe' } ) ) ;
148+ expect ( execa . sync ) . toHaveBeenNthCalledWith ( 2 , 'npm' , [ 'run' , 'build' ] , expect . objectContaining ( { stdio : 'pipe' } ) ) ;
149+ } ) ;
150+
151+ it ( 'should not call install or tsc separately when build script is defined' , async ( ) => {
152+ JSONUtilities_mock . readJson . mockReturnValue ( {
153+ scripts : {
154+ build : 'custom-build-command' ,
155+ } ,
156+ } ) ;
157+
158+ await buildCustomResources ( mockContext ) ;
159+
160+ // Verify install and tsc are NOT called
161+ const calls = ( execa . sync as jest . Mock ) . mock . calls ;
162+ const hasInstallCall = calls . some ( ( call ) => call [ 1 ] ?. [ 0 ] === 'install' ) ;
163+ const hasTscCall = calls . some ( ( call ) => call [ 1 ] ?. [ 0 ] === 'tsc' ) ;
164+
165+ expect ( hasInstallCall ) . toBe ( false ) ;
166+ expect ( hasTscCall ) . toBe ( false ) ;
167+ } ) ;
168+ } ) ;
169+
170+ describe ( 'hasBuildScript edge cases' , ( ) => {
171+ it ( 'should fall back to install+tsc when package.json does not exist' , async ( ) => {
172+ // package.json doesn't exist for the custom resource
173+ ( fs_mock . existsSync as jest . Mock ) . mockImplementation ( ( filePath : unknown ) => {
174+ if ( typeof filePath === 'string' && filePath . includes ( 'package.json' ) ) {
175+ return false ;
176+ }
177+ return true ;
178+ } ) ;
179+
180+ await buildCustomResources ( mockContext ) ;
77181
78- // 2 for npm install and 2 for tsc build (1 per resource)
79- expect ( execa . sync ) . toBeCalledTimes ( 4 ) ;
182+ // Should run install + tsc (default behavior)
183+ expect ( execa . sync ) . toBeCalledTimes ( 4 ) ;
184+ } ) ;
80185 } ) ;
81186} ) ;
0 commit comments