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

Commit

Permalink
fix: modify type field of transpiled ESM packages (#783)
Browse files Browse the repository at this point in the history
* refactor: rename internal variables

* feat: add `rewrites` object

* refactor: load esbuild output from memory

* feat: modify package.json of transpiled ESM packages

* chore: update tests

* chore: remove unused import
  • Loading branch information
eduardoboucas committed Nov 2, 2021
1 parent 7e5772d commit c9d3337
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 66 deletions.
18 changes: 18 additions & 0 deletions src/runtimes/node/bundlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,25 @@ type BundleFunction = (
repositoryRoot?: string
} & FunctionSource,
) => Promise<{
// Aliases are used to change the path that a file should take inside the
// generated archive. For example:
//
// "/my-transpiled-function.js" => "/my-function.js"
//
// When "/my-transpiled-function.js" is found in the list of files, it will
// be added to the archive with the "/my-function.js" path.
aliases?: Map<string, string>

// Rewrites are used to change the source file associated with a given path.
// For example:
//
// "/my-function.js" => "console.log(`Hello!`)"
//
// When "/my-function.js" is found in the list of files, it will be added to
// the archive with "console.log(`Hello!`)" as its source, replacing whatever
// the file at "/my-function.js" contains.
rewrites?: Map<string, string>

basePath: string
bundlerWarnings?: BundlerWarning[]
cleanupFunction?: CleanupFunction
Expand Down
52 changes: 52 additions & 0 deletions src/runtimes/node/bundlers/nft/es_modules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { resolve } from 'path'

import { NodeFileTraceReasons } from '@vercel/nft'

import { cachedReadFile, FsCache } from '../../../../utils/fs'
import { PackageJson } from '../../utils/package_json'

const getESMPackageJsons = (esmPaths: Set<string>, reasons: NodeFileTraceReasons, basePath?: string) => {
const packageJsons: string[] = [...reasons.entries()]
.filter(([, reason]) => {
if (reason.type !== 'resolve') {
return false
}

const hasESMParent = [...reason.parents].some((parentPath) => esmPaths.has(parentPath))

return hasESMParent
})
.map(([path]) => (basePath ? resolve(basePath, path) : resolve(path)))

return packageJsons
}

const getPatchedESMPackages = async (
esmPaths: Set<string>,
reasons: NodeFileTraceReasons,
fsCache: FsCache,
basePath?: string,
) => {
const packages = getESMPackageJsons(esmPaths, reasons, basePath)
const patchedPackages = await Promise.all(packages.map((path) => patchESMPackage(path, fsCache)))
const patchedPackagesMap = new Map()

packages.forEach((packagePath, index) => {
patchedPackagesMap.set(packagePath, patchedPackages[index])
})

return patchedPackagesMap
}

const patchESMPackage = async (path: string, fsCache: FsCache) => {
const file = (await cachedReadFile(fsCache, path, 'utf8')) as string
const packageJson: PackageJson = JSON.parse(file)
const patchedPackageJson = {
...packageJson,
type: 'commonjs',
}

return JSON.stringify(patchedPackageJson)
}

export { getPatchedESMPackages }
57 changes: 16 additions & 41 deletions src/runtimes/node/bundlers/nft/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,17 @@ import unixify from 'unixify'

import type { BundleFunction } from '..'
import type { FunctionConfig } from '../../../../config'
import { cachedReadFile, FsCache, safeUnlink } from '../../../../utils/fs'
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 { getPatchedESMPackages } from './es_modules'
import { transpileMany } from './transpile'

// Paths that will be excluded from the tracing process.
const ignore = ['node_modules/aws-sdk/**']

interface NftCache {
analysisCache?: Map<string, { isESM: boolean; [key: string]: unknown }>
[key: string]: unknown
}

const appearsToBeModuleName = (name: string) => !name.startsWith('.')

const bundle: BundleFunction = async ({
Expand All @@ -36,11 +32,7 @@ const bundle: BundleFunction = async ({
includedFiles,
includedFilesBasePath || basePath,
)
const {
cleanupFunction,
paths: dependencyPaths,
transpilation,
} = await traceFilesAndTranspile({
const { paths: dependencyPaths, rewrites } = await traceFilesAndTranspile({
basePath: repositoryRoot,
config,
mainFile,
Expand All @@ -50,14 +42,13 @@ const bundle: BundleFunction = async ({
const dirnames = filteredIncludedPaths.map((filePath) => normalize(dirname(filePath))).sort()

// Sorting the array to make the checksum deterministic.
const srcFiles = [...filteredIncludedPaths, ...transpilation.keys()].sort()
const srcFiles = [...filteredIncludedPaths].sort()

return {
aliases: transpilation,
basePath: getBasePath(dirnames),
cleanupFunction,
inputs: dependencyPaths,
mainFile,
rewrites,
srcFiles,
}
}
Expand All @@ -81,10 +72,12 @@ const traceFilesAndTranspile = async function ({
pluginsModulesPath?: string
}) {
const fsCache: FsCache = {}
const cache: NftCache = {}
const { fileList: dependencyPaths } = await nodeFileTrace([mainFile], {
const {
fileList: dependencyPaths,
esmFileList,
reasons,
} = await nodeFileTrace([mainFile], {
base: basePath,
cache,
ignore: ignoreFunction,
readFile: async (path: string) => {
try {
Expand Down Expand Up @@ -119,32 +112,14 @@ const traceFilesAndTranspile = async function ({
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))
}
const esmPaths = [...esmFileList].map((path) => (basePath ? resolve(basePath, path) : resolve(path)))
const transpiledPaths = await transpileMany(esmPaths, config)
const patchedESMPackages = await getPatchedESMPackages(esmFileList, reasons, fsCache, basePath)
const rewrites = new Map([...transpiledPaths, ...patchedESMPackages])

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

Expand Down
17 changes: 4 additions & 13 deletions src/runtimes/node/bundlers/nft/transpile.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
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({
const transpiled = await build({
bundle: false,
entryPoints: [path],
format: 'cjs',
logLevel: 'error',
outfile: targetPath,
platform: 'node',
target: [nodeTarget],
write: false,
})

return {
cleanupFn,
path: targetPath,
}
return transpiled.outputFiles[0].text
}

const transpileMany = async (paths: string[], config: FunctionConfig) => {
Expand All @@ -36,7 +27,7 @@ const transpileMany = async (paths: string[], config: FunctionConfig) => {
paths.map(async (path) => {
const transpiled = await transpile(path, config)

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

Expand Down
2 changes: 2 additions & 0 deletions src/runtimes/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const zipFunction: ZipFunction = async function ({
mainFile: finalMainFile = mainFile,
nativeNodeModules,
nodeModulesWithDynamicImports,
rewrites,
srcFiles,
} = await bundler.bundle({
basePath,
Expand All @@ -122,6 +123,7 @@ const zipFunction: ZipFunction = async function ({
filename,
mainFile: finalMainFile,
pluginsModulesPath,
rewrites,
srcFiles,
})

Expand Down
1 change: 1 addition & 0 deletions src/runtimes/node/utils/package_json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface PackageJson {
files?: string[]
gypfile?: boolean
binary?: boolean
type?: string
}

// Retrieve the `package.json` of a specific project or module
Expand Down
40 changes: 31 additions & 9 deletions src/runtimes/node/utils/zip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ interface ZipNodeParameters {
filename: string
mainFile: string
pluginsModulesPath?: string
rewrites?: Map<string, string>
srcFiles: string[]
}

Expand All @@ -49,6 +50,7 @@ const createDirectory = async function ({
filename,
mainFile,
pluginsModulesPath,
rewrites = new Map(),
srcFiles,
}: ZipNodeParameters) {
const { contents: entryContents, filename: entryFilename } = getEntryFile({
Expand All @@ -70,16 +72,20 @@ const createDirectory = async function ({
await pMap(
srcFiles,
(srcFile) => {
const srcPath = aliases.get(srcFile) || srcFile
const normalizedSrcPath = normalizeFilePath({
const destPath = aliases.get(srcFile) || srcFile
const normalizedDestPath = normalizeFilePath({
commonPrefix: basePath,
path: srcPath,
path: destPath,
pluginsModulesPath,
userNamespace: DEFAULT_USER_SUBDIRECTORY,
})
const destPath = join(functionFolder, normalizedSrcPath)
const absoluteDestPath = join(functionFolder, normalizedDestPath)

return copyFile(srcFile, destPath)
if (rewrites.has(srcFile)) {
return pWriteFile(absoluteDestPath, rewrites.get(srcFile) as string)
}

return copyFile(srcFile, absoluteDestPath)
},
{ concurrency: COPY_FILE_CONCURRENCY },
)
Expand All @@ -95,6 +101,7 @@ const createZipArchive = async function ({
filename,
mainFile,
pluginsModulesPath,
rewrites,
srcFiles,
}: ZipNodeParameters) {
const destPath = join(destFolder, `${basename(filename, extension)}.zip`)
Expand Down Expand Up @@ -126,7 +133,16 @@ const createZipArchive = async function ({
// We ensure this is not async, so that the archive's checksum is
// deterministic. Otherwise it depends on the order the files were added.
srcFilesInfos.forEach(({ srcFile, stat }) => {
zipJsFile({ srcFile, commonPrefix: basePath, pluginsModulesPath, archive, stat, aliases, userNamespace })
zipJsFile({
aliases,
archive,
commonPrefix: basePath,
pluginsModulesPath,
rewrites,
srcFile,
stat,
userNamespace,
})
})

await endZip(archive, output)
Expand Down Expand Up @@ -183,6 +199,7 @@ const zipJsFile = function ({
archive,
commonPrefix,
pluginsModulesPath,
rewrites = new Map(),
stat,
srcFile,
userNamespace,
Expand All @@ -191,14 +208,19 @@ const zipJsFile = function ({
archive: ZipArchive
commonPrefix: string
pluginsModulesPath?: string
rewrites?: Map<string, string>
stat: Stats
srcFile: string
userNamespace: string
}) {
const filename = aliases.get(srcFile) || srcFile
const normalizedFilename = normalizeFilePath({ commonPrefix, path: filename, pluginsModulesPath, userNamespace })
const destPath = aliases.get(srcFile) || srcFile
const normalizedDestPath = normalizeFilePath({ commonPrefix, path: destPath, pluginsModulesPath, userNamespace })

addZipFile(archive, srcFile, normalizedFilename, stat)
if (rewrites.has(srcFile)) {
addZipContent(archive, rewrites.get(srcFile) as string, normalizedDestPath)
} else {
addZipFile(archive, srcFile, normalizedDestPath, stat)
}
}

// `adm-zip` and `require()` expect Unix paths.
Expand Down
4 changes: 3 additions & 1 deletion tests/fixtures/local-require-esm/function/file.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import esm from 'esm-module'

export default function getZero() {
return 0
return esm() && 0
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions tests/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -425,18 +425,23 @@ testMany(
'Can bundle functions with `.js` extension using ES Modules and feature flag OFF',
['bundler_esbuild', 'bundler_default', 'bundler_nft'],
async (options, t) => {
const fixtureName = 'local-require-esm'
const opts = merge(options, {
basePath: `${FIXTURES_DIR}/${fixtureName}`,
featureFlags: { defaultEsModulesToEsbuild: false },
})
const bundler = options.config['*'].nodeBundler

await (bundler === undefined
? t.throwsAsync(
zipNode(t, 'local-require-esm', {
length: 3,
opts: merge(options, { featureFlags: { defaultEsModulesToEsbuild: false } }),
opts,
}),
)
: zipNode(t, 'local-require-esm', {
length: 3,
opts: merge(options, { featureFlags: { defaultEsModulesToEsbuild: false } }),
opts,
}))
},
)
Expand Down

1 comment on commit c9d3337

@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: 9s

largeDepsNft: 1m 2.4s

largeDepsZisi: 1m 12.9s

Please sign in to comment.