Skip to content
This repository has been archived by the owner on May 22, 2024. It is now read-only.

feat: generate pure ESM functions for NFT #1004

Merged
merged 6 commits into from
Feb 8, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ const bundle: BundleFunction = async ({
bundlerWarnings,
inputs,
mainFile: normalizedMainFile,
moduleFormat: 'cjs',
Copy link
Member Author

Choose a reason for hiding this comment

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

To be addressed in a follow-up PR.

nativeNodeModules,
nodeModulesWithDynamicImports,
srcFiles: [...supportingSrcFiles, ...bundlePaths.keys()],
Expand Down
40 changes: 38 additions & 2 deletions src/runtimes/node/bundlers/index.ts
Original file line number Diff line number Diff line change
@@ -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 ({
Copy link
Member Author

Choose a reason for hiding this comment

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

This function was moved from src/runtimes/node/index.ts. The code was not changed.

extension,
mainFile,
featureFlags,
}: {
extension: string
mainFile: string
featureFlags: FeatureFlags
}): Promise<NodeBundlerName> => {
const { defaultEsModulesToEsbuild, traceWithNft } = featureFlags

if (['.mjs', '.ts'].includes(extension)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Note for a future PR: maybe we should support *.mts too.

Copy link
Member Author

Choose a reason for hiding this comment

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

Definitely!

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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const bundle: BundleFunction = async ({
basePath: getBasePath(dirnames),
inputs: srcFiles,
mainFile,
moduleFormat: 'cjs',
Copy link
Member Author

Choose a reason for hiding this comment

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

To be addressed in a follow-up PR.

srcFiles,
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/runtimes/node/bundlers/zisi/list_imports.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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