Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions packages/build/src/plugins_core/functions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { resolve } from 'path'

import { NodeBundlerName, RUNTIME, zipFunctions } from '@netlify/zip-it-and-ship-it'
import { NodeBundlerName, RUNTIME, zipFunctions, FunctionResult } from '@netlify/zip-it-and-ship-it'
import { pathExists } from 'path-exists'

import { addErrorInfo } from '../../error/info.js'
Expand All @@ -13,7 +13,7 @@ import { getUserAndInternalFunctions, validateFunctionsSrc } from './utils.js'
import { getZisiParameters } from './zisi.js'

// Get a list of all unique bundlers in this run
const getBundlers = (results: Awaited<ReturnType<typeof zipFunctions>> = []) =>
const getBundlers = (results: FunctionResult[] = []) =>
// using a Set to filter duplicates
new Set(
results
Expand All @@ -38,7 +38,7 @@ const eventTriggeredFunctions = new Set([
'identity-login',
])

const validateCustomRoutes = function (functions: Awaited<ReturnType<typeof zipFunctions>>) {
const validateCustomRoutes = function (functions: FunctionResult[]) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really related to the metadata file, but it's good to use the imported types from zip-it-and-ship-it.

for (const { routes, name, schedule } of functions) {
if (!routes || routes.length === 0) continue

Expand All @@ -61,6 +61,7 @@ const validateCustomRoutes = function (functions: Awaited<ReturnType<typeof zipF
}

const zipFunctionsAndLogResults = async ({
branch,
buildDir,
childEnv,
featureFlags,
Expand All @@ -76,6 +77,7 @@ const zipFunctionsAndLogResults = async ({
systemLog,
}) => {
const zisiParameters = getZisiParameters({
branch,
buildDir,
childEnv,
featureFlags,
Expand Down Expand Up @@ -118,6 +120,7 @@ const coreStep = async function ({
FUNCTIONS_DIST: relativeFunctionsDist,
},
buildDir,
branch,
packagePath,
logs,
netlifyConfig,
Expand Down Expand Up @@ -166,6 +169,7 @@ const coreStep = async function ({
}

const { bundlers } = await zipFunctionsAndLogResults({
branch,
buildDir,
childEnv,
featureFlags,
Expand Down Expand Up @@ -237,7 +241,7 @@ export const bundleFunctions = {
// `zip-it-and-ship-it` methods. Therefore, we need to use an intermediary
// function and export them so tests can use it.
export const zipItAndShipIt = {
async zipFunctions(...args: Parameters<typeof zipFunctions>) {
async zipFunctions(...args: Parameters<typeof zipFunctions>): Promise<FunctionResult[]> {
return await zipFunctions(...args)
},
}
Expand Down
3 changes: 3 additions & 0 deletions packages/build/src/plugins_core/functions/zisi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { FeatureFlags } from '../../core/feature_flags.js'
import { getZisiFeatureFlags } from './feature_flags.js'

type GetZisiParametersType = {
branch?: string
buildDir: string
childEnv: Record<string, string>
featureFlags: FeatureFlags
Expand Down Expand Up @@ -40,6 +41,7 @@ const getLambdaNodeVersion = (childEnv: Record<string, string>, userNodeVersion:
}

export const getZisiParameters = ({
branch,
buildDir,
childEnv,
featureFlags,
Expand All @@ -65,6 +67,7 @@ export const getZisiParameters = ({

return {
basePath: buildDir,
branch,
config,
manifest,
featureFlags: zisiFeatureFlags,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default async () => new Response("Hello")

export const config = { path: "/hello" }
40 changes: 38 additions & 2 deletions packages/build/tests/functions/tests.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { readdir, readFile, rm, stat, writeFile } from 'fs/promises'
import { resolve } from 'path'
import { join, resolve } from 'path'
import { version as nodeVersion } from 'process'
import { fileURLToPath } from 'url'

import { Fixture, normalizeOutput, removeDir, getTempName } from '@netlify/testing'
import { Fixture, normalizeOutput, removeDir, getTempName, unzipFile } from '@netlify/testing'
import test from 'ava'
import { pathExists } from 'path-exists'
import semver from 'semver'
Expand Down Expand Up @@ -204,3 +204,39 @@ if (semver.gte(nodeVersion, '16.9.0')) {
t.true(app2FunctionsDist.includes('worker.zip'))
})
}

test('Functions: creates metadata file', async (t) => {
const fixture = await new Fixture('./fixtures/v2').withCopyRoot({ git: false })
const build = await fixture
.withFlags({
branch: 'my-branch',
cwd: fixture.repositoryRoot,
featureFlags: { zisi_add_metadata_file: true },
})
.runWithBuildAndIntrospect()

t.true(build.success)

const functionsDistPath = resolve(fixture.repositoryRoot, '.netlify/functions')
const functionsDistFiles = await readdir(functionsDistPath)

t.true(functionsDistFiles.includes('manifest.json'))
t.true(functionsDistFiles.includes('test.zip'))

const unzipPath = join(functionsDistPath, `.netlify-test-${Date.now()}`)

await unzipFile(join(functionsDistPath, 'test.zip'), unzipPath)

const functionFiles = await readdir(unzipPath)

t.true(functionFiles.includes('___netlify-bootstrap.mjs'))
t.true(functionFiles.includes('___netlify-entry-point.mjs'))
t.true(functionFiles.includes('___netlify-metadata.json'))
t.true(functionFiles.includes('test.mjs'))

const metadata = JSON.parse(await readFile(join(unzipPath, '___netlify-metadata.json'), 'utf8'))

t.is(semver.valid(metadata.bootstrap_version), metadata.bootstrap_version)
t.is(metadata.branch, 'my-branch')
t.is(metadata.version, 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should we also delete the unzipped file? or will it get cleaned up eventually?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think these directories are being cleaned up at the moment. I think that's okay since in the context of an ephemeral CI it sometimes does more harm than good, as you need to spend time deleting a bunch of files. I think it would be nice to do it optionally for a local dev setup, but I'd rather handle that more holistically as part of the Fixture class and not on this specific test.

Something for us to consider!

})
14 changes: 14 additions & 0 deletions packages/testing/src/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { mkdir } from 'fs/promises'
import { platform } from 'process'

import { execa } from 'execa'

export const unzipFile = async function (path: string, dest: string): Promise<void> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've extracted this function from @netlify/zip-it-and-ship-it so it can be used by any package in the monorepo.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to update @netlify/zip-it-and-ship-it to pull the function from @netlify/testing, but sadly I'm getting a circular dependency error. For now, I'm keeping the function in both places.

await mkdir(dest, { recursive: true })

if (platform === 'win32') {
await execa('tar', ['-xf', path, '-C', dest])
} else {
await execa('unzip', ['-o', path, '-d', dest])
}
}
1 change: 1 addition & 0 deletions packages/testing/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './dir.js'
export * from './fixture.js'
export * from './fs.js'
export * from './normalize.js'
export * from './server.js'
export * from './tcp_server.js'
Expand Down
4 changes: 2 additions & 2 deletions packages/zip-it-and-ship-it/src/feature_flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export const defaultFlags = {
// Adds the `___netlify-telemetry.mjs` file to the function bundle.
zisi_add_instrumentation_loader: true,

// Adds a `___netlify-bootstrap-version` file to the function bundle.
zisi_add_version_file: false,
// Adds a `___netlify-metadata.json` file to the function bundle.
zisi_add_metadata_file: false,
} as const

export type FeatureFlags = Partial<Record<keyof typeof defaultFlags, boolean>>
Expand Down
2 changes: 2 additions & 0 deletions packages/zip-it-and-ship-it/src/runtimes/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const getSrcFilesWithBundler: GetSrcFilesFunction = async (parameters) => {
const zipFunction: ZipFunction = async function ({
archiveFormat,
basePath,
branch,
cache,
config = {},
destFolder,
Expand Down Expand Up @@ -113,6 +114,7 @@ const zipFunction: ZipFunction = async function ({
aliases,
archiveFormat,
basePath: finalBasePath,
branch,
cache,
destFolder,
extension,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { normalizeFilePath } from './normalize_path.js'
export const ENTRY_FILE_NAME = '___netlify-entry-point'
export const BOOTSTRAP_FILE_NAME = '___netlify-bootstrap.mjs'
export const BOOTSTRAP_VERSION_FILE_NAME = '___netlify-bootstrap-version'
export const METADATA_FILE_NAME = '___netlify-metadata.json'
export const TELEMETRY_FILE_NAME = '___netlify-telemetry.mjs'

const require = createRequire(import.meta.url)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface MetadataFile {
bootstrap_version?: string
branch?: string
version: number
}

export const getMetadataFile = (bootstrapVersion?: string, branch?: string): MetadataFile => ({
bootstrap_version: bootstrapVersion,
branch,
version: 1,
})
12 changes: 7 additions & 5 deletions packages/zip-it-and-ship-it/src/runtimes/node/utils/zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ import { cachedLstat, mkdirAndWriteFile } from '../../../utils/fs.js'

import {
BOOTSTRAP_FILE_NAME,
BOOTSTRAP_VERSION_FILE_NAME,
METADATA_FILE_NAME,
conflictsWithEntryFile,
EntryFile,
getEntryFile,
getTelemetryFile,
isNamedLikeEntryFile,
} from './entry_file.js'
import { getMetadataFile } from './metadata_file.js'
import { ModuleFormat } from './module_format.js'
import { normalizeFilePath } from './normalize_path.js'
import { getPackageJsonIfAvailable } from './package_json.js'
Expand All @@ -44,6 +45,7 @@ const DEFAULT_USER_SUBDIRECTORY = 'src'
interface ZipNodeParameters {
aliases?: Map<string, string>
basePath: string
branch?: string
cache: RuntimeCache
destFolder: string
extension: string
Expand Down Expand Up @@ -186,6 +188,7 @@ const createDirectory = async function ({
const createZipArchive = async function ({
aliases = new Map(),
basePath,
branch,
cache,
destFolder,
extension,
Expand Down Expand Up @@ -251,12 +254,11 @@ const createZipArchive = async function ({
if (runtimeAPIVersion === 2) {
const bootstrapPath = addBootstrapFile(srcFiles, aliases)

if (featureFlags.zisi_add_version_file === true) {
if (featureFlags.zisi_add_metadata_file === true) {
const { version: bootstrapVersion } = await getPackageJsonIfAvailable(bootstrapPath)
const payload = JSON.stringify(getMetadataFile(bootstrapVersion, branch))

if (bootstrapVersion) {
addZipContent(archive, bootstrapVersion, BOOTSTRAP_VERSION_FILE_NAME)
}
addZipContent(archive, payload, METADATA_FILE_NAME)
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/zip-it-and-ship-it/src/runtimes/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export type ZipFunction = (
args: {
archiveFormat: ArchiveFormat
basePath?: string
branch?: string
cache: RuntimeCache
config: FunctionConfig
destFolder: string
Expand Down
9 changes: 6 additions & 3 deletions packages/zip-it-and-ship-it/src/zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ import { getFunctionsFromPaths } from './runtimes/index.js'
import { MODULE_FORMAT } from './runtimes/node/utils/module_format.js'
import { addArchiveSize } from './utils/archive_size.js'
import { RuntimeCache } from './utils/cache.js'
import { formatZipResult } from './utils/format_result.js'
import { formatZipResult, FunctionResult } from './utils/format_result.js'
import { listFunctionsDirectories, resolveFunctionsDirectories } from './utils/fs.js'
import { getLogger, LogFunction } from './utils/logger.js'
import { nonNullable } from './utils/non_nullable.js'

export interface ZipFunctionOptions {
archiveFormat?: ArchiveFormat
basePath?: string
branch?: string
config?: Config
featureFlags?: FeatureFlags
repositoryRoot?: string
Expand Down Expand Up @@ -53,6 +54,7 @@ export const zipFunctions = async function (
{
archiveFormat = ARCHIVE_FORMAT.ZIP,
basePath,
branch,
config = {},
configFileDirectories,
featureFlags: inputFeatureFlags,
Expand All @@ -63,7 +65,7 @@ export const zipFunctions = async function (
debug,
internalSrcFolder,
}: ZipFunctionsOptions = {},
) {
): Promise<FunctionResult[]> {
validateArchiveFormat(archiveFormat)

const logger = getLogger(systemLog, debug)
Expand Down Expand Up @@ -94,6 +96,7 @@ export const zipFunctions = async function (
const zipResult = await func.runtime.zipFunction({
archiveFormat,
basePath,
branch,
cache,
config: func.config,
destFolder,
Expand Down Expand Up @@ -145,7 +148,7 @@ export const zipFunction = async function (
debug,
internalSrcFolder,
}: ZipFunctionOptions = {},
) {
): Promise<FunctionResult | undefined> {
validateArchiveFormat(archiveFormat)

const logger = getLogger(systemLog, debug)
Expand Down
54 changes: 40 additions & 14 deletions packages/zip-it-and-ship-it/tests/v2api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,22 +709,48 @@ describe.runIf(semver.gte(nodeVersion, '18.13.0'))('V2 functions API', () => {
expect(files[0].runtimeVersion).toBe('nodejs20.x')
})

test('Adds a file with the bootstrap version to the ZIP archive', async () => {
const fixtureName = 'v2-api'
const { files } = await zipFixture(fixtureName, {
fixtureDir: FIXTURES_ESM_DIR,
opts: {
featureFlags: {
zisi_add_version_file: true,
describe('Adds a file with metadata', () => {
test('Without a branch', async () => {
const fixtureName = 'v2-api'
const { files } = await zipFixture(fixtureName, {
fixtureDir: FIXTURES_ESM_DIR,
opts: {
featureFlags: {
zisi_add_metadata_file: true,
},
},
},
})
const [unzippedFunction] = await unzipFiles(files)
const bootstrapPath = getBootstrapPath()
const bootstrapPackageJson = await readFile(resolve(bootstrapPath, '..', '..', 'package.json'), 'utf8')
const { version: bootstrapVersion } = JSON.parse(bootstrapPackageJson)
const versionFileContents = await readFile(join(unzippedFunction.unzipPath, '___netlify-metadata.json'), 'utf8')

expect(JSON.parse(versionFileContents)).toEqual({ bootstrap_version: bootstrapVersion, version: 1 })
})
const [unzippedFunction] = await unzipFiles(files)
const bootstrapPath = getBootstrapPath()
const bootstrapPackageJson = await readFile(resolve(bootstrapPath, '..', '..', 'package.json'), 'utf8')
const { version: bootstrapVersion } = JSON.parse(bootstrapPackageJson)
const versionFileContents = await readFile(join(unzippedFunction.unzipPath, '___netlify-bootstrap-version'), 'utf8')

expect(versionFileContents).toBe(bootstrapVersion)
test('With a branch', async () => {
const fixtureName = 'v2-api'
const { files } = await zipFixture(fixtureName, {
fixtureDir: FIXTURES_ESM_DIR,
opts: {
branch: 'main',
featureFlags: {
zisi_add_metadata_file: true,
},
},
})
const [unzippedFunction] = await unzipFiles(files)
const bootstrapPath = getBootstrapPath()
const bootstrapPackageJson = await readFile(resolve(bootstrapPath, '..', '..', 'package.json'), 'utf8')
const { version: bootstrapVersion } = JSON.parse(bootstrapPackageJson)
const versionFileContents = await readFile(join(unzippedFunction.unzipPath, '___netlify-metadata.json'), 'utf8')

expect(JSON.parse(versionFileContents)).toEqual({
bootstrap_version: bootstrapVersion,
branch: 'main',
version: 1,
})
})
})
})
Loading