From d0c6d9f816d927e9476309034e2e2ef8f7d0c2e1 Mon Sep 17 00:00:00 2001 From: Nathan Houle Date: Fri, 5 Sep 2025 11:45:01 -0700 Subject: [PATCH] [WIP] refactor(config): parse cached config into definite shape yadda yadda yadda, the cached config shape is untyped yadda yadda yadda, we don't parse it when it exists to make sure it's the right shape yadda yadda yadda, we should tho --- packages/config/src/cached_config.js | 29 ------ packages/config/src/cached_config.ts | 129 +++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 29 deletions(-) delete mode 100644 packages/config/src/cached_config.js create mode 100644 packages/config/src/cached_config.ts diff --git a/packages/config/src/cached_config.js b/packages/config/src/cached_config.js deleted file mode 100644 index 4268445f87..0000000000 --- a/packages/config/src/cached_config.js +++ /dev/null @@ -1,29 +0,0 @@ -import { promises as fs } from 'fs' - -// Performance optimization when @netlify/config caller has already previously -// called it and cached the result. -// This is used by the buildbot which: -// - first calls @netlify/config since it needs configuration property -// - later calls @netlify/build, which runs @netlify/config under the hood -// This is also used by Netlify CLI. -export const getCachedConfig = async function ({ cachedConfig, cachedConfigPath, token, api }) { - const parsedCachedConfig = await parseCachedConfig(cachedConfig, cachedConfigPath) - // The CLI does not print some properties when they are not serializable or - // are confidential. Those will be missing from `cachedConfig`. The caller - // must supply them again when using `cachedConfig`. - return parsedCachedConfig === undefined ? undefined : { token, ...parsedCachedConfig, api } -} - -// `cachedConfig` is a plain object while `cachedConfigPath` is a file path to -// a JSON file. The former is useful in programmatic usage while the second one -// is useful in CLI usage, to avoid hitting the OS limits if the configuration -// file is too big. -const parseCachedConfig = async function (cachedConfig, cachedConfigPath) { - if (cachedConfig !== undefined) { - return cachedConfig - } - - if (cachedConfigPath !== undefined) { - return JSON.parse(await fs.readFile(cachedConfigPath)) - } -} diff --git a/packages/config/src/cached_config.ts b/packages/config/src/cached_config.ts new file mode 100644 index 0000000000..02ef1d4320 --- /dev/null +++ b/packages/config/src/cached_config.ts @@ -0,0 +1,129 @@ +import fs from 'node:fs/promises' + +import type { NetlifyAPI } from '@netlify/api' +import * as z from 'zod' + +// TODO: There are likely fields missing here, and many of these fields are probably optional. +export const SerializedCachedConfigSchema = z.object({ + accounts: z.array( + z.object({ + id: z.string(), + name: z.string(), + slug: z.string(), + default: z.boolean(), + team_logo_url: z.string().nullable(), + on_pro_trial: z.boolean(), + organization_id: z.string().nullable(), + type_name: z.string(), + type_slug: z.string(), + members_count: z.number(), + }), + ), + branch: z.string(), + buildDir: z.string(), + config: z.object({ + build: z.object({ + edge_functions: z.string().optional(), + environment: z.record( + z.string(), + z.object({ + sources: z.array(z.string()), // z.array(z.enum(['internal', 'ui', ...])), + value: z.string(), + }), + ), + functions: z.string().optional(), + publish: z.string(), + publishOrigin: z.string(), // z.enum(['config']), + processing: z.record(z.string(), z.unknown()), + services: z.record(z.string(), z.unknown()), + command: z.string(), + commandOrigin: z.string(), // z.enum(['config', ...]) + }), + functions: z.record(z.string(), z.record(z.string(), z.unknown())), + functionsDirectory: z.string().optional(), + functionsDirectoryOrigin: z.string().optional(), // z.enum(['config', 'default', 'default-v1', 'ui', ...]), + plugins: z.array( + z.object({ + origin: z.string(), // z.enum(['config', ...]) + package: z.string(), + inputs: z.record(z.string(), z.unknown()), + }), + ), + headers: z.array(z.unknown()), + redirects: z.array(z.unknown()), + }), + configPath: z.string(), + context: z.string(), // z.enum(['production', 'deploy-preview', 'branch-deploy', 'dev', 'dev-server']), + env: z.record( + z.string(), + z.object({ + sources: z.array(z.string()), // z.array(z.enum(['internal', 'ui', ...])), + value: z.string(), + }), + ), + integrations: z.array( + z.object({ + author: z.string(), + buildPlugin: z.object({ + origin: z.string(), + packageURL: z.string(), + }), + extension_token: z.string(), + has_build: z.boolean(), + name: z.string(), + slug: z.string(), + version: z.string(), + }), + ), + hasApi: z.boolean().optional(), + headersPath: z.string(), + siteInfo: z.record(z.string(), z.unknown()), // z.object({ id: z.string(), account_id: z.string() }), + redirectsPath: z.string(), + repositoryRoot: z.string(), +}) + +// export type SerializedCachedConfig = z.output + +// Performance optimization when @netlify/config caller has already previously +// called it and cached the result. +// This is used by the buildbot which: +// - first calls @netlify/config since it needs configuration property +// - later calls @netlify/build, which runs @netlify/config under the hood +// This is also used by Netlify CLI. +export const getCachedConfig = async function ({ + api, + cachedConfig, + cachedConfigPath, + token, +}: { + api: NetlifyAPI | undefined + cachedConfig: unknown + cachedConfigPath: string | undefined + token: string | undefined +}): // eslint-disable-next-line @typescript-eslint/no-explicit-any +Promise { + const parsedCachedConfig = await parseCachedConfig(cachedConfig, cachedConfigPath) + // The CLI does not print some properties when they are not serializable or + // are confidential. Those will be missing from `cachedConfig`. The caller + // must supply them again when using `cachedConfig`. + return parsedCachedConfig === undefined ? undefined : { token, ...parsedCachedConfig, api } +} + +// `cachedConfig` is a plain object while `cachedConfigPath` is a file path to +// a JSON file. The former is useful in programmatic usage while the second one +// is useful in CLI usage, to avoid hitting the OS limits if the configuration +// file is too big. +const parseCachedConfig = async function ( + cachedConfig: unknown, + cachedConfigPath: string | undefined, +): Promise { + if (cachedConfig !== undefined) { + // return cachedConfig + return SerializedCachedConfigSchema.parse(cachedConfig) + } + + if (cachedConfigPath !== undefined) { + // return JSON.parse(await fs.readFile(cachedConfigPath, 'utf8')) + return SerializedCachedConfigSchema.parse(JSON.parse(await fs.readFile(cachedConfigPath, 'utf8'))) + } +}