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

feat: add ESM support when bundling with NFT #759

Merged
merged 5 commits into from
Oct 22, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
114 changes: 81 additions & 33 deletions src/runtimes/node/bundlers/nft/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,98 @@ import { dirname, normalize, resolve } from 'path'
import { nodeFileTrace } from '@vercel/nft'

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

const bundle: BundleFunction = async ({
basePath,
config,
extension,
featureFlags,
filename,
mainFile,
name,
pluginsModulesPath,
repositoryRoot = basePath,
runtime,
srcDir,
srcPath,
stat,
}) => {
const srcFiles = await getSrcFiles({
import { transpileMany } from './transpile'
import type { Cache } from './types'

const bundle: BundleFunction = async ({ basePath, config, mainFile, repositoryRoot = basePath }) => {
const { includedFiles = [], includedFilesBasePath } = config
const { exclude: excludedPaths, paths: includedFilePaths } = await getPathsOfIncludedFiles(
includedFiles,
includedFilesBasePath || basePath,
)
const {
cleanupFunction,
paths: dependencyPaths,
transpilation,
} = await traceFilesAndTranspile({
basePath: repositoryRoot,
config: {
...config,
includedFilesBasePath: config.includedFilesBasePath || basePath,
},
extension,
featureFlags,
filename,
config,
mainFile,
name,
pluginsModulesPath,
repositoryRoot,
runtime,
srcDir,
srcPath,
stat,
})
const dirnames = srcFiles.map((filePath) => normalize(dirname(filePath))).sort()
const filteredIncludedPaths = filterExcludedPaths([...dependencyPaths, ...includedFilePaths], excludedPaths)
const dirnames = filteredIncludedPaths.map((filePath) => normalize(dirname(filePath))).sort()

return {
aliases: transpilation,
basePath: getBasePath(dirnames),
inputs: srcFiles,
cleanupFunction,
inputs: dependencyPaths,
mainFile,
srcFiles,
srcFiles: [...filteredIncludedPaths, ...transpilation.keys()],
}
}

const traceFilesAndTranspile = async function ({
basePath,
config,
mainFile,
}: {
basePath?: string
config: FunctionConfig
mainFile: string
}) {
const fsCache: FsCache = {}
const cache: Cache = {}
const { fileList: dependencyPaths } = await nodeFileTrace([mainFile], {
base: basePath,
cache,
readFile: async (path: string) => {
try {
const source = (await cachedReadFile(fsCache, path, 'utf8')) as string

return source
} catch (error) {
if (error.code === 'ENOENT' || error.code === 'EISDIR') {
return null
}

throw error
}
},
})
const normalizedDependencyPaths = dependencyPaths.map((path) => (basePath ? resolve(basePath, path) : resolve(path)))

// We look at the cache object to find any paths corresponding to ESM files.
const esmPaths = [...(cache.analysisCache?.entries() || [])].filter(([, { isESM }]) => isESM).map(([path]) => path)

// After transpiling the ESM files, we get back a `Map` mapping the path of
// each transpiled to its original path.
const transpilation = await transpileMany(esmPaths, config)

// Creating a `Set` with the original paths of the transpiled files so that
// we can do a O(1) lookup.
const originalPaths = new Set(transpilation.values())

// We remove the transpiled paths from the list of traced files, otherwise we
// would end up with duplicate files in the archive.
const filteredDependencyPaths = normalizedDependencyPaths.filter((path) => !originalPaths.has(path))

// The cleanup function will delete all the temporary files that were created
// as part of the transpilation process.
const cleanupFunction = async () => {
await Promise.all([...transpilation.keys()].map(safeUnlink))
}

return {
cleanupFunction,
paths: filteredDependencyPaths,
transpilation,
}
}

Expand Down
46 changes: 46 additions & 0 deletions src/runtimes/node/bundlers/nft/transpile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { build } from '@netlify/esbuild'
import { tmpName } from 'tmp-promise'

import type { FunctionConfig } from '../../../../config'
import { safeUnlink } from '../../../../utils/fs'
import { getBundlerTarget } from '../esbuild/bundler_target'

const transpile = async (path: string, config: FunctionConfig) => {
const targetPath = await tmpName({ postfix: '.js' })
const cleanupFn = () => safeUnlink(targetPath)

// The version of ECMAScript to use as the build target. This will determine
// whether certain features are transpiled down or left untransformed.
const nodeTarget = getBundlerTarget(config.nodeVersion)

await build({
bundle: false,
entryPoints: [path],
format: 'cjs',
logLevel: 'error',
outfile: targetPath,
platform: 'node',
target: [nodeTarget],
})

return {
cleanupFn,
path: targetPath,
}
}

const transpileMany = async (paths: string[], config: FunctionConfig) => {
const transpiledPaths: Map<string, string> = new Map()

await Promise.all(
paths.map(async (path) => {
const transpiled = await transpile(path, config)

transpiledPaths.set(transpiled.path, path)
}),
)

return transpiledPaths
}

export { transpileMany }
6 changes: 6 additions & 0 deletions src/runtimes/node/bundlers/nft/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
interface Cache {
eduardoboucas marked this conversation as resolved.
Show resolved Hide resolved
analysisCache?: Map<string, { isESM: boolean; [key: string]: unknown }>
[key: string]: unknown
}

export { Cache }
6 changes: 3 additions & 3 deletions tests/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ testMany(

testMany(
'Can bundle functions with `.js` extension using ES Modules and feature flag ON',
['bundler_esbuild', 'bundler_default', 'todo:bundler_nft'],
['bundler_esbuild', 'bundler_default', 'bundler_nft'],
async (options, t) => {
const opts = merge(options, { featureFlags: { defaultEsModulesToEsbuild: true } })

Expand All @@ -411,7 +411,7 @@ testMany(

testMany(
'Can bundle functions with `.js` extension using ES Modules and feature flag OFF',
['bundler_esbuild', 'bundler_default', 'todo:bundler_nft'],
['bundler_esbuild', 'bundler_default', 'bundler_nft'],
async (options, t) => {
const bundler = options.config['*'].nodeBundler

Expand Down Expand Up @@ -1206,7 +1206,7 @@ testMany(

testMany(
'Handles a JavaScript function ({name}.mjs, {name}/{name}.mjs, {name}/index.mjs)',
['bundler_esbuild', 'bundler_default', 'todo:bundler_nft'],
netlify-team-account-1 marked this conversation as resolved.
Show resolved Hide resolved
['bundler_esbuild', 'bundler_default'],
async (options, t) => {
const { files, tmpDir } = await zipFixture(t, 'node-mjs', {
length: 3,
Expand Down