import { DeploymentFile } from './hashes' ; import { FetchOptions } from '@zeit/fetch' ; import { nodeFetch , zeitFetch } from './fetch' ; import { join , sep , relative , posix } from 'path' ; import { URL } from 'url' ; import ignore from 'ignore' ; type Ignore = ReturnType < typeof ignore > ; import { pkgVersion } from '../pkg' ; import { NowBuildError } from '@vercel/build-utils' ; import { VercelClientOptions , DeploymentOptions , NowConfig } from '../types' ; import { Sema } from 'async-sema' ; import { readFile } from 'fs-extra' ; import readdir from 'recursive-readdir' ; const semaphore = new Sema ( 10 ) ; export const API_FILES = '/v2/now/files' ; const EVENTS_ARRAY = [ 'hashes-calculated' , 'file-count' , 'file-uploaded' , 'all-files-uploaded' , 'created' , 'building' , 'ready' , 'alias-assigned' , 'warning' , 'error' , 'notice' , 'tip' , 'canceled' , 'checks-registered' , 'checks-completed' , 'checks-running' , 'checks-conclusion-succeeded' , 'checks-conclusion-failed' , 'checks-conclusion-skipped' , 'checks-conclusion-canceled' , ] as const ; export type DeploymentEventType = typeof EVENTS_ARRAY [ number ] ; export const EVENTS = new Set ( EVENTS_ARRAY ) ; export function getApiDeploymentsUrl ( metadata ? ) { if ( metadata && metadata . builds && ! metadata . functions ) { return '/v10/now/deployments' ; } return '/v13/now/deployments' ; } export async function parseVercelConfig ( filePath ? ) { if ( ! filePath ) { return { } ; } try { const jsonString = await readFile ( filePath , 'utf8' ) ; return JSON . parse ( jsonString ) ; } catch ( e ) { console . error ( e ) ; return { } ; } } const maybeRead = async function < T > ( path , default_ ) { try { return await readFile ( path , 'utf8' ) ; } catch ( err ) { return default_ ; } } ; export async function buildFileTree ( path , { isDirectory , prebuilt , rootDirectory , } , debug ) { const ignoreList = [ ] ; let fileList ; let { ig , ignores } = await getVercelIgnore ( path , prebuilt , rootDirectory ) ; debug ( `Found ${ ignores . length } rules in .vercelignore` ) ; debug ( 'Building file tree...' ) ; if ( isDirectory && ! Array . isArray ( path ) ) { const ignores = ( absPath ) => { const rel = relative ( path , absPath ) ; const ignored = ig . ignores ( rel ) ; if ( ignored ) { ignoreList . push ( rel ) ; } return ignored ; } ; fileList = await readdir ( path , [ ignores ] ) ; debug ( `Found ${ fileList . length } files in the specified directory` ) ; } else if ( Array . isArray ( path ) ) { fileList = path ; debug ( `Assigned ${ fileList . length } files provided explicitly` ) ; } else { fileList = [ path ] ; debug ( `Deploying the provided path as single file` ) ; } return { fileList , ignoreList } ; } export async function getVercelIgnore ( cwd , prebuilt ? , rootDirectory ? ) { let ignores = [ ] ; const outputDir = posix . join ( rootDirectory || '' , '.output' ) ; if ( prebuilt ) { ignores . push ( '*' ) ; const parts = outputDir . split ( '/' ) ; parts . forEach ( ( _ , i ) => { const level = parts . slice ( 0 , i + 1 ) . join ( '/' ) ; ignores . push ( `!${ level }` ) ; } ) ; ignores . push ( `!${ outputDir }/**` ) ; } else { ignores = [ '.hg' , '.git' , '.gitmodules' , '.svn' , '.cache' , '.next' , '.now' , '.vercel' , '.npmignore' , '.dockerignore' , '.gitignore' , '.*.swp' , '.DS_Store' , '.wafpicke-*' , '.lock-wscript' , '.env.local' , '.env.*.local' , '.venv' , 'npm-debug.log' , 'config.gypi' , 'node_modules' , '__pycache__' , 'venv' , 'CVS' , `.output` , ] ; } const cwds = Array . isArray ( cwd ) ? cwd : [ cwd ] ; const files = await Promise . all ( cwds . map ( async ( cwd ) => { const [ vercelignore , nowignore ] = await Promise . all ( [ maybeRead ( join ( cwd , '.vercelignore' ) , '' ) , maybeRead ( join ( cwd , '.nowignore' ) , '' ) , ] ) ; if ( vercelignore && nowignore ) { throw new NowBuildError ( { code : 'CONFLICTING_IGNORE_FILES' , message : 'Cannot use both a `.vercelignore` and `.nowignore` file. Please delete the `.nowignore` file.' , link : 'https://vercel.link/combining-old-and-new-config' , } ) ; } return vercelignore || nowignore ; } ) ) ; const ignoreFile = files . join ( '\n' ) ; const ig = ignore ( ) . add ( `${ ignores . join ( '\n' ) }\n${ clearRelative ( ignoreFile ) }` ) ; return { ig , ignores } ; } /**
 * Remove leading `./` from the beginning of ignores
 * because ignore doesn't like them :|
 */ function clearRelative ( str ) { return str . replace ( /(\n|^)\.\//g , '$1' ) ; } interface FetchOpts extends FetchOptions { apiUrl ? : string ; method ? : string ; teamId ? : string ; headers ? : { [ key ] : any ; } ; userAgent ? : string ; } export const fetch = async ( url , token , opts = { } , debugEnabled ? , useNodeFetch ? ) : Promise < any > => { semaphore . acquire ( ) ; const debug = createDebug ( debugEnabled ) ; let time ; url = `${ opts . apiUrl || 'https://api.vercel.com' }${ url }` ; delete opts . apiUrl ; if ( opts . teamId ) { const parsedUrl = new URL ( url ) ; parsedUrl . searchParams . set ( 'teamId' , opts . teamId ) ; url = parsedUrl . toString ( ) ; delete opts . teamId ; } const userAgent = opts . userAgent || `client-v${ pkgVersion }` ; delete opts . userAgent ; opts . headers = { ... opts . headers , authorization : `Bearer ${ token }` , accept : 'application/json' , 'user-agent' : userAgent , } ; debug ( `${ opts . method || 'GET' } ${ url }` ) ; time = Date . now ( ) ; const res = useNodeFetch ? await nodeFetch ( url , opts ) : await zeitFetch ( url , opts ) ; debug ( `DONE in ${ Date . now ( ) - time }ms: ${ opts . method || 'GET' } ${ url }` ) ; semaphore . release ( ) ; return res ; } ; export interface PreparedFile { file : string ; sha : string ; size : number ; mode : number ; } const isWin = process . platform . includes ( 'win' ) ; export const prepareFiles = ( files , clientOptions ) : PreparedFile [ ] => { const preparedFiles = [ ] ; for ( const [ sha , file ] of files ) { for ( const name of file . names ) { let fileName ; if ( clientOptions . isDirectory ) { fileName = typeof clientOptions . path === 'string' ? relative ( clientOptions . path , name ) : name ; } else { const segments = name . split ( sep ) ; fileName = segments [ segments . length - 1 ] ; } preparedFiles . push ( { file : isWin ? fileName . replace ( /\\/g , '/' ) : fileName , size : file . data . byteLength || file . data . length , mode : file . mode , sha , } ) ; } } return preparedFiles ; } ; export function createDebug ( debug ? ) { if ( debug ) { return ( ... logs ) => { process . stderr . write ( [ `[client-debug] ${ new Date ( ) . toISOString ( ) }` , ... logs ] . join ( ' ' ) + '\n' ) ; } ; } return ( ) => { } ; } type Debug = ReturnType < typeof createDebug > ;