Skip to content

Commit

Permalink
feat: generate pure ESM functions for NFT (#1004)
Browse files Browse the repository at this point in the history
* feat: generate ESM entrypoints

* chore: fix linting errors

* chore: fix test

* feat: add `zisi_pure_esm` feature flag

* chore: fix tests

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
eduardoboucas and kodiakhq[bot] committed Feb 8, 2022
1 parent 56db245 commit cac2058
Show file tree
Hide file tree
Showing 20 changed files with 362 additions and 117 deletions.
5 changes: 3 additions & 2 deletions src/config.ts
Expand Up @@ -2,7 +2,8 @@ import mergeOptions from 'merge-options'
import minimatch from 'minimatch'

import { FunctionSource } from './function'
import type { NodeBundlerName, NodeVersion } from './runtimes/node'
import type { NodeVersionString } from './runtimes/node'
import type { NodeBundlerName } from './runtimes/node/bundlers'

interface FunctionConfig {
externalNodeModules?: string[]
Expand All @@ -11,7 +12,7 @@ interface FunctionConfig {
ignoredNodeModules?: string[]
nodeBundler?: NodeBundlerName
nodeSourcemap?: boolean
nodeVersion?: NodeVersion
nodeVersion?: NodeVersionString
processDynamicNodeImports?: boolean
rustTargetDirectory?: string
schedule?: string
Expand Down
1 change: 1 addition & 0 deletions src/feature_flags.ts
Expand Up @@ -6,6 +6,7 @@ const FLAGS: Record<string, boolean> = {
defaultEsModulesToEsbuild: Boolean(env.NETLIFY_EXPERIMENTAL_DEFAULT_ES_MODULES_TO_ESBUILD),
parseWithEsbuild: false,
traceWithNft: false,
zisi_pure_esm: false,
}

type FeatureFlag = keyof typeof FLAGS
Expand Down
2 changes: 1 addition & 1 deletion src/runtimes/node/bundlers/esbuild/bundler.ts
Expand Up @@ -3,7 +3,7 @@ import { basename, dirname, extname, resolve, join } from 'path'
import { build, Metafile } from '@netlify/esbuild'
import { tmpName } from 'tmp-promise'

import type { NodeBundlerName } from '../..'
import type { NodeBundlerName } from '..'
import type { FunctionConfig } from '../../../../config'
import { getPathWithExtension, safeUnlink } from '../../../../utils/fs'
import type { RuntimeName } from '../../../runtime'
Expand Down
1 change: 1 addition & 0 deletions src/runtimes/node/bundlers/esbuild/index.ts
Expand Up @@ -122,6 +122,7 @@ const bundle: BundleFunction = async ({
bundlerWarnings,
inputs,
mainFile: normalizedMainFile,
moduleFormat: 'cjs',
nativeNodeModules,
nodeModulesWithDynamicImports,
srcFiles: [...supportingSrcFiles, ...bundlePaths.keys()],
Expand Down
40 changes: 38 additions & 2 deletions src/runtimes/node/bundlers/index.ts
@@ -1,14 +1,17 @@
import type { Message } from '@netlify/esbuild'

import type { NodeBundlerName } from '..'
import { FunctionConfig } from '../../../config'
import { FeatureFlag, FeatureFlags } from '../../../feature_flags'
import { FunctionSource } from '../../../function'
import { detectEsModule } from '../utils/detect_es_module'
import { ModuleFormat } from '../utils/module_format'

import esbuildBundler from './esbuild'
import nftBundler from './nft'
import zisiBundler from './zisi'

export type NodeBundlerName = 'esbuild' | 'esbuild_zisi' | 'nft' | 'zisi'

// TODO: Create a generic warning type
type BundlerWarning = Message

Expand Down Expand Up @@ -49,6 +52,7 @@ type BundleFunction = (
cleanupFunction?: CleanupFunction
inputs: string[]
mainFile: string
moduleFormat: ModuleFormat
nativeNodeModules?: NativeNodeModules
nodeModulesWithDynamicImports?: string[]
srcFiles: string[]
Expand Down Expand Up @@ -86,5 +90,37 @@ const getBundler = (name: NodeBundlerName): NodeBundler => {
}
}

export { getBundler }
// We use ZISI as the default bundler, except for certain extensions, for which
// esbuild is the only option.
const getDefaultBundler = async ({
extension,
mainFile,
featureFlags,
}: {
extension: string
mainFile: string
featureFlags: FeatureFlags
}): Promise<NodeBundlerName> => {
const { defaultEsModulesToEsbuild, traceWithNft } = featureFlags

if (['.mjs', '.ts'].includes(extension)) {
return 'esbuild'
}

if (traceWithNft) {
return 'nft'
}

if (defaultEsModulesToEsbuild) {
const isEsModule = await detectEsModule({ mainFile })

if (isEsModule) {
return 'esbuild'
}
}

return 'zisi'
}

export { getBundler, getDefaultBundler }
export type { BundleFunction, GetSrcFilesFunction, NativeNodeModules }
71 changes: 67 additions & 4 deletions src/runtimes/node/bundlers/nft/es_modules.ts
@@ -1,10 +1,13 @@
import { basename, resolve } from 'path'
import { basename, dirname, resolve } from 'path'

import { NodeFileTraceReasons } from '@vercel/nft'

import type { FunctionConfig } from '../../../../config'
import { FeatureFlags } from '../../../../feature_flags'
import { cachedReadFile, FsCache } from '../../../../utils/fs'
import { PackageJson } from '../../utils/package_json'
import { ModuleFormat } from '../../utils/module_format'
import { getNodeSupportMatrix } from '../../utils/node_version'
import { getPackageJson, PackageJson } from '../../utils/package_json'

import { transpile } from './transpile'

Expand All @@ -19,6 +22,21 @@ const getPatchedESMPackages = async (packages: string[], fsCache: FsCache) => {
return patchedPackagesMap
}

const isEntrypointESM = ({
basePath,
esmPaths,
mainFile,
}: {
basePath?: string
esmPaths: Set<string>
mainFile: string
}) => {
const absoluteESMPaths = new Set([...esmPaths].map((path) => resolvePath(path, basePath)))
const entrypointIsESM = absoluteESMPaths.has(mainFile)

return entrypointIsESM
}

const patchESMPackage = async (path: string, fsCache: FsCache) => {
const file = (await cachedReadFile(fsCache, path, 'utf8')) as string
const packageJson: PackageJson = JSON.parse(file)
Expand All @@ -30,6 +48,51 @@ const patchESMPackage = async (path: string, fsCache: FsCache) => {
return JSON.stringify(patchedPackageJson)
}

const processESM = async ({
basePath,
config,
esmPaths,
featureFlags,
fsCache,
mainFile,
reasons,
}: {
basePath: string | undefined
config: FunctionConfig
esmPaths: Set<string>
featureFlags: FeatureFlags
fsCache: FsCache
mainFile: string
reasons: NodeFileTraceReasons
}): Promise<{ rewrites?: Map<string, string>; moduleFormat: ModuleFormat }> => {
const entrypointIsESM = isEntrypointESM({ basePath, esmPaths, mainFile })

if (!entrypointIsESM) {
return {
moduleFormat: 'cjs',
}
}

const packageJson = await getPackageJson(dirname(mainFile))
const nodeSupport = getNodeSupportMatrix(config.nodeVersion)

if (featureFlags.zisi_pure_esm && packageJson.type === 'module' && nodeSupport.esm) {
return {
moduleFormat: 'esm',
}
}

const rewrites = await transpileESM({ basePath, config, esmPaths, fsCache, reasons })

return {
moduleFormat: 'cjs',
rewrites,
}
}

const resolvePath = (relativePath: string, basePath?: string) =>
basePath ? resolve(basePath, relativePath) : resolve(relativePath)

const shouldTranspile = (
path: string,
cache: Map<string, boolean>,
Expand Down Expand Up @@ -101,7 +164,7 @@ const transpileESM = async ({

await Promise.all(
pathsToTranspile.map(async (path) => {
const absolutePath = basePath ? resolve(basePath, path) : resolve(path)
const absolutePath = resolvePath(path, basePath)
const transpiled = await transpile(absolutePath, config)

rewrites.set(absolutePath, transpiled)
Expand All @@ -111,4 +174,4 @@ const transpileESM = async ({
return rewrites
}

export { transpileESM }
export { processESM }
25 changes: 22 additions & 3 deletions src/runtimes/node/bundlers/nft/index.ts
Expand Up @@ -7,12 +7,13 @@ import unixify from 'unixify'

import type { BundleFunction } from '..'
import type { FunctionConfig } from '../../../../config'
import { FeatureFlags } from '../../../../feature_flags'
import { cachedReadFile, FsCache } from '../../../../utils/fs'
import type { GetSrcFilesFunction } from '../../../runtime'
import { getBasePath } from '../../utils/base_path'
import { filterExcludedPaths, getPathsOfIncludedFiles } from '../../utils/included_files'

import { transpileESM } from './es_modules'
import { processESM } from './es_modules'

// Paths that will be excluded from the tracing process.
const ignore = ['node_modules/aws-sdk/**']
Expand All @@ -22,6 +23,7 @@ const appearsToBeModuleName = (name: string) => !name.startsWith('.')
const bundle: BundleFunction = async ({
basePath,
config,
featureFlags,
mainFile,
pluginsModulesPath,
repositoryRoot = basePath,
Expand All @@ -31,9 +33,14 @@ const bundle: BundleFunction = async ({
includedFiles,
includedFilesBasePath || basePath,
)
const { paths: dependencyPaths, rewrites } = await traceFilesAndTranspile({
const {
moduleFormat,
paths: dependencyPaths,
rewrites,
} = await traceFilesAndTranspile({
basePath: repositoryRoot,
config,
featureFlags,
mainFile,
pluginsModulesPath,
})
Expand All @@ -47,6 +54,7 @@ const bundle: BundleFunction = async ({
basePath: getBasePath(dirnames),
inputs: dependencyPaths,
mainFile,
moduleFormat,
rewrites,
srcFiles,
}
Expand All @@ -62,11 +70,13 @@ const ignoreFunction = (path: string) => {
const traceFilesAndTranspile = async function ({
basePath,
config,
featureFlags,
mainFile,
pluginsModulesPath,
}: {
basePath?: string
config: FunctionConfig
featureFlags: FeatureFlags
mainFile: string
pluginsModulesPath?: string
}) {
Expand Down Expand Up @@ -111,9 +121,18 @@ const traceFilesAndTranspile = async function ({
const normalizedDependencyPaths = [...dependencyPaths].map((path) =>
basePath ? resolve(basePath, path) : resolve(path),
)
const rewrites = await transpileESM({ basePath, config, esmPaths: esmFileList, fsCache, reasons })
const { moduleFormat, rewrites } = await processESM({
basePath,
config,
esmPaths: esmFileList,
featureFlags,
fsCache,
mainFile,
reasons,
})

return {
moduleFormat,
paths: normalizedDependencyPaths,
rewrites,
}
Expand Down
1 change: 1 addition & 0 deletions src/runtimes/node/bundlers/zisi/index.ts
Expand Up @@ -42,6 +42,7 @@ const bundle: BundleFunction = async ({
basePath: getBasePath(dirnames),
inputs: srcFiles,
mainFile,
moduleFormat: 'cjs',
srcFiles,
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/runtimes/node/bundlers/zisi/list_imports.ts
Expand Up @@ -2,7 +2,7 @@ import * as esbuild from '@netlify/esbuild'
import isBuiltinModule from 'is-builtin-module'
import { tmpName } from 'tmp-promise'

import type { NodeBundlerName } from '../..'
import type { NodeBundlerName } from '..'
import { safeUnlink } from '../../../../utils/fs'
import type { RuntimeName } from '../../../runtime'

Expand Down
41 changes: 4 additions & 37 deletions src/runtimes/node/index.ts
Expand Up @@ -2,50 +2,15 @@ import { join } from 'path'

import cpFile from 'cp-file'

import { FeatureFlags } from '../../feature_flags'
import { GetSrcFilesFunction, Runtime, ZipFunction } from '../runtime'

import { getBundler } from './bundlers'
import { getBundler, getDefaultBundler } from './bundlers'
import { findFunctionsInPaths, findFunctionInPath } from './finder'
import { findISCDeclarationsInPath } from './in_source_config'
import { detectEsModule } from './utils/detect_es_module'
import { createAliases as createPluginsModulesPathAliases, getPluginsModulesPath } from './utils/plugin_modules_path'
import { zipNodeJs } from './utils/zip'

export type NodeBundlerName = 'esbuild' | 'esbuild_zisi' | 'nft' | 'zisi'
export { NodeVersion } from './utils/node_version'

// We use ZISI as the default bundler, except for certain extensions, for which
// esbuild is the only option.
const getDefaultBundler = async ({
extension,
mainFile,
featureFlags,
}: {
extension: string
mainFile: string
featureFlags: FeatureFlags
}): Promise<NodeBundlerName> => {
const { defaultEsModulesToEsbuild, traceWithNft } = featureFlags

if (['.mjs', '.ts'].includes(extension)) {
return 'esbuild'
}

if (traceWithNft) {
return 'nft'
}

if (defaultEsModulesToEsbuild) {
const isEsModule = await detectEsModule({ mainFile })

if (isEsModule) {
return 'esbuild'
}
}

return 'zisi'
}
export { NodeVersionString } from './utils/node_version'

// A proxy for the `getSrcFiles` function which adds a default `bundler` using
// the `getDefaultBundler` function.
Expand Down Expand Up @@ -98,6 +63,7 @@ const zipFunction: ZipFunction = async function ({
bundlerWarnings,
inputs,
mainFile: finalMainFile = mainFile,
moduleFormat,
nativeNodeModules,
nodeModulesWithDynamicImports,
rewrites,
Expand Down Expand Up @@ -130,6 +96,7 @@ const zipFunction: ZipFunction = async function ({
extension,
filename,
mainFile: finalMainFile,
moduleFormat,
rewrites,
srcFiles,
})
Expand Down

1 comment on commit cac2058

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

⏱ Benchmark results

largeDepsEsbuild: 6.6s

largeDepsNft: 30.1s

largeDepsZisi: 56.1s

Please sign in to comment.