From 573165c922f20c355dd4ec49fcd69358386d6f19 Mon Sep 17 00:00:00 2001 From: Philippe Serhal Date: Fri, 16 Oct 2026 05:36:56 -0400 Subject: [PATCH] refactor(deploy): remove options and guards the types make dead Batch 2 of 3 of follow-ups to #8599 (batch 1 here: #8605). Out of an abundance of caution, that PR was kept purely focused on type changes. - Drop runtime checks for options the types now require - Remove the unused `assetType` option from file hashing - Only upload in-memory bodies for static files - Every upload built its body with `body ?? fs.createReadStream(filepath)`, although only the generated `netlify.toml` (an `InlineUploadFile`) ever carries an in-memory body. Static files still upload their inline `body` when present and stream `filepath` otherwise; functions, edge functions and servers now always stream from `filepath`. - Build the functions config with `Object.fromEntries` - The per-function config payload sent to the API was accumulated with a spreading `reduce`. It is now built by a named `toFunctionConfigPayload` mapper and `Object.fromEntries`, which also gives the payload a derived type. --- src/commands/deploy/deploy.ts | 5 +- src/utils/deploy/deploy-site.ts | 3 -- src/utils/deploy/hash-config.ts | 3 +- src/utils/deploy/hash-files.ts | 8 +-- src/utils/deploy/hash-fns.ts | 84 ++++++++++++----------------- src/utils/deploy/hasher-segments.ts | 10 +--- src/utils/deploy/upload-files.ts | 18 +++---- 7 files changed, 48 insertions(+), 83 deletions(-) diff --git a/src/commands/deploy/deploy.ts b/src/commands/deploy/deploy.ts index fd4873d52fe..d4ec0104c96 100644 --- a/src/commands/deploy/deploy.ts +++ b/src/commands/deploy/deploy.ts @@ -279,10 +279,7 @@ const getDeployFilesFilter = ({ deployFolder, site }: { deployFolder: string; si // when site.root !== deployFolder the behaviour matches our buildbot const skipNodeModules = site.root === deployFolder - return (filename: string | null | undefined) => { - if (filename == null) { - return false - } + return (filename: string) => { if (filename === deployFolder) { return true } diff --git a/src/utils/deploy/deploy-site.ts b/src/utils/deploy/deploy-site.ts index dc3ec2c1a45..f961a5ac43b 100644 --- a/src/utils/deploy/deploy-site.ts +++ b/src/utils/deploy/deploy-site.ts @@ -50,7 +50,6 @@ const buildStatsString = (possibleParts: (string | false | undefined)[]) => { } export interface DeploySiteOptions { - assetType?: 'file' | undefined branch?: string concurrentHash?: number concurrentUpload?: number @@ -83,7 +82,6 @@ export const deploySite = async ( siteId: string, dir: string, { - assetType, branch, concurrentHash = DEFAULT_CONCURRENT_HASH, concurrentUpload = DEFAULT_CONCURRENT_UPLOAD, @@ -127,7 +125,6 @@ export const deploySite = async ( { edgeFunctions, edgeFnShaMap }, ] = await Promise.all([ hashFiles({ - assetType, concurrentHash, directories: [dir, edgeFunctionsDistPath, deployConfigPath, dbMigrationsDistPath].filter((d): d is string => Boolean(d), diff --git a/src/utils/deploy/hash-config.ts b/src/utils/deploy/hash-config.ts index f2d68b44662..3dbfac463d2 100644 --- a/src/utils/deploy/hash-config.ts +++ b/src/utils/deploy/hash-config.ts @@ -5,8 +5,7 @@ import tomlify from 'tomlify-j0.4' import type { DeployConfig } from './types.js' import type { InlineUploadFile } from './upload-files.js' -export const hashConfig = ({ config }: { config: DeployConfig | undefined }): InlineUploadFile & { hash: string } => { - if (!config) throw new Error('Missing config option') +export const hashConfig = ({ config }: { config: DeployConfig }): InlineUploadFile & { hash: string } => { const configString = serializeToml(config) const hash = createHash('sha1').update(configString).digest('hex') diff --git a/src/utils/deploy/hash-files.ts b/src/utils/deploy/hash-files.ts index dd56700812c..b2c7d4ddcff 100644 --- a/src/utils/deploy/hash-files.ts +++ b/src/utils/deploy/hash-files.ts @@ -7,7 +7,6 @@ import { fileFilterCtor, fileNormalizerCtor, hasherCtor, manifestCollectorCtor } import type { StatusCallback } from './status-cb.js' const hashFiles = async ({ - assetType = 'file', concurrentHash, directories, filter, @@ -15,20 +14,17 @@ const hashFiles = async ({ normalizer, statusCb, }: { - assetType?: 'file' | undefined concurrentHash: number directories: string[] - filter: ((filename: string) => boolean) | undefined + filter: (filename: string) => boolean hashAlgorithm?: string | undefined normalizer?: (file: File) => File statusCb: StatusCallback }): Promise<{ files: Record; filesShaMap: Record }> => { - if (!filter) throw new Error('Missing filter function option') - const fileStream = walker(directories, { filter }) const fileFilter = fileFilterCtor() const hasher = hasherCtor({ concurrentHash, hashAlgorithm }) - const fileNormalizer = fileNormalizerCtor({ assetType, normalizer }) + const fileNormalizer = fileNormalizerCtor({ normalizer }) // Written to by manifestCollector // normalizedPath: hash (wanted by deploy API) diff --git a/src/utils/deploy/hash-fns.ts b/src/utils/deploy/hash-fns.ts index 01c56291a0d..62f77b503ef 100644 --- a/src/utils/deploy/hash-fns.ts +++ b/src/utils/deploy/hash-fns.ts @@ -163,18 +163,20 @@ const trafficRulesConfig = (trafficRules?: TrafficRules) => { } } -interface FunctionConfigPayload { - display_name: FunctionResult['displayName'] - excluded_routes: FunctionResult['excludedRoutes'] - generator: FunctionResult['generator'] - memory: FunctionResult['memory'] - region: FunctionResult['region'] - routes: FunctionResult['routes'] - build_data: FunctionResult['buildData'] - priority: FunctionResult['priority'] - traffic_rules: ReturnType - vcpu: FunctionResult['vcpu'] -} +const toFunctionConfigPayload = (func: FunctionResult) => ({ + display_name: func.displayName, + excluded_routes: func.excludedRoutes, + generator: func.generator, + memory: func.memory, + region: func.region, + routes: func.routes, + build_data: func.buildData, + priority: func.priority, + traffic_rules: trafficRulesConfig(func.trafficRules), + vcpu: func.vcpu, +}) + +type FunctionConfigPayload = ReturnType const hashFns = async ( command: BaseCommand, @@ -202,13 +204,12 @@ const hashFns = async ( serverManifestPath?: string | undefined skipFunctionsCache?: boolean | undefined statusCb: StatusCallback - tmpDir?: string | undefined + tmpDir: string }, ): Promise<{ functionSchedules?: { name: string; cron: string }[] | undefined functions: Record functionsWithNativeModules: FunctionResult[] - shaMap?: Record | undefined fnShaMap?: Record | undefined fnConfig?: Record | undefined server?: { sha: string; region?: string } | undefined @@ -217,11 +218,7 @@ const hashFns = async ( // Exit early if there is nothing to bundle. A site can have a server without // any functions directory. if (directories.length === 0 && !serverEnabled) { - return { functions: {}, functionsWithNativeModules: [], shaMap: {} } - } - - if (!tmpDir) { - throw new Error('Missing tmpDir directory for zipping files') + return { functions: {}, functionsWithNativeModules: [] } } const [functionZips, serverBundle] = await Promise.all([ @@ -282,38 +279,23 @@ const hashFns = async ( trafficRules, }), ) - const fnConfig = functionZips - .filter((func) => - Boolean( - func.displayName || - func.generator || - func.routes || - func.buildData || - func.priority || - func.trafficRules || - func.region || - func.memory || - func.vcpu, - ), - ) - .reduce>( - (funcs, curr) => ({ - ...funcs, - [curr.name]: { - display_name: curr.displayName, - excluded_routes: curr.excludedRoutes, - generator: curr.generator, - memory: curr.memory, - region: curr.region, - routes: curr.routes, - build_data: curr.buildData, - priority: curr.priority, - traffic_rules: trafficRulesConfig(curr.trafficRules), - vcpu: curr.vcpu, - }, - }), - {}, - ) + const fnConfig = Object.fromEntries( + functionZips + .filter((func) => + Boolean( + func.displayName || + func.generator || + func.routes || + func.buildData || + func.priority || + func.trafficRules || + func.region || + func.memory || + func.vcpu, + ), + ) + .map((func) => [func.name, toFunctionConfigPayload(func)]), + ) const functionSchedules = functionZips .map(({ name, schedule }) => schedule && { name, cron: schedule }) .filter((schedule): schedule is { name: string; cron: string } => schedule !== '' && schedule !== undefined) diff --git a/src/utils/deploy/hasher-segments.ts b/src/utils/deploy/hasher-segments.ts index 897169a4afb..4f769189859 100644 --- a/src/utils/deploy/hasher-segments.ts +++ b/src/utils/deploy/hasher-segments.ts @@ -34,17 +34,11 @@ export const hasherCtor = ({ concurrentHash, hashAlgorithm }: { concurrentHash?: } // Inject normalized file names into normalizedPath and assetType -export const fileNormalizerCtor = ({ - assetType, - normalizer: normalizeFunction, -}: { - assetType: 'file' - normalizer?: (file: File) => File -}) => { +export const fileNormalizerCtor = ({ normalizer: normalizeFunction }: { normalizer?: (file: File) => File }) => { return new Transform({ objectMode: true, transform(fileObj: HashedFile, _, callback) { - const normalizedFile: File = { ...fileObj, assetType, normalizedPath: normalizePath(fileObj.relname) } + const normalizedFile: File = { ...fileObj, assetType: 'file', normalizedPath: normalizePath(fileObj.relname) } const result = normalizeFunction !== undefined ? normalizeFunction(normalizedFile) : normalizedFile diff --git a/src/utils/deploy/upload-files.ts b/src/utils/deploy/upload-files.ts index 9bee2c632ec..d2aa52b5f1e 100644 --- a/src/utils/deploy/upload-files.ts +++ b/src/utils/deploy/upload-files.ts @@ -24,11 +24,11 @@ type UploadDeployServerParams = WithRetryCount { - const { body, filepath, normalizedPath } = fileObj - - // FIXME(@netlify/api): octet-stream `body` is typed as `ReadStream` only, but an inline string body works too - const readStreamCtor = () => (body ?? fs.createReadStream(filepath)) as fs.ReadStream + const { normalizedPath } = fileObj statusCb({ type: 'upload', @@ -107,10 +104,13 @@ const uploadFiles = async ( switch (fileObj.assetType) { case 'file': { + const { body, filepath } = fileObj return await retryUpload( () => api.uploadDeployFile({ - body: readStreamCtor, + // FIXME(@netlify/api): `types.d.ts` hardcodes every `application/octet-stream` body as `ReadStream | (() => ReadStream)`, + // ignoring the spec's `string` type; a string body works at runtime + body: () => (body ?? fs.createReadStream(filepath)) as fs.ReadStream, deployId, path: encodeURI(normalizedPath), }), @@ -122,7 +122,7 @@ const uploadFiles = async ( return await retryUpload((retryCount) => { const params: UploadDeployFunctionParams = { - body: readStreamCtor, + body: () => fs.createReadStream(fileObj.filepath), deployId, invocationMode, timeout, @@ -140,7 +140,7 @@ const uploadFiles = async ( case 'edge-function': { return await retryUpload((retryCount) => { const params: UploadDeployEdgeFunctionParams = { - body: readStreamCtor, + body: () => fs.createReadStream(fileObj.filepath), deployId, codeSha: normalizedPath, } @@ -155,7 +155,7 @@ const uploadFiles = async ( case 'server': { return await retryUpload((retryCount) => { const params: UploadDeployServerParams = { - body: readStreamCtor, + body: () => fs.createReadStream(fileObj.filepath), deployId, codeSha: fileObj.hash, }