-
Notifications
You must be signed in to change notification settings - Fork 35
fix: modify type
field of transpiled ESM packages
#783
Changes from all commits
77ea2fa
47a30f0
2c97c0a
cfb016f
cd3dd3f
c04aa9a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
basePath: string | ||
bundlerWarnings?: BundlerWarning[] | ||
cleanupFunction?: CleanupFunction | ||
|
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 } |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 ({ | ||
|
@@ -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, | ||
|
@@ -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, | ||
} | ||
} | ||
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We were previously relying on NFT's cache object to inspect which of the paths corresponded to ESM files, but I realised there's a |
||
reasons, | ||
} = await nodeFileTrace([mainFile], { | ||
base: basePath, | ||
cache, | ||
ignore: ignoreFunction, | ||
readFile: async (path: string) => { | ||
try { | ||
|
@@ -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, | ||
} | ||
} | ||
|
||
|
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({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We were piping the output of esbuild to a temporary file, but actually we can just get the result loaded into memory by setting |
||
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) => { | ||
|
@@ -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) | ||
}), | ||
) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,6 +38,7 @@ interface ZipNodeParameters { | |
filename: string | ||
mainFile: string | ||
pluginsModulesPath?: string | ||
rewrites?: Map<string, string> | ||
srcFiles: string[] | ||
} | ||
|
||
|
@@ -49,6 +50,7 @@ const createDirectory = async function ({ | |
filename, | ||
mainFile, | ||
pluginsModulesPath, | ||
rewrites = new Map(), | ||
srcFiles, | ||
}: ZipNodeParameters) { | ||
const { contents: entryContents, filename: entryFilename } = getEntryFile({ | ||
|
@@ -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 }, | ||
) | ||
|
@@ -95,6 +101,7 @@ const createZipArchive = async function ({ | |
filename, | ||
mainFile, | ||
pluginsModulesPath, | ||
rewrites, | ||
srcFiles, | ||
}: ZipNodeParameters) { | ||
const destPath = join(destFolder, `${basename(filename, extension)}.zip`) | ||
|
@@ -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) | ||
|
@@ -183,6 +199,7 @@ const zipJsFile = function ({ | |
archive, | ||
commonPrefix, | ||
pluginsModulesPath, | ||
rewrites = new Map(), | ||
stat, | ||
srcFile, | ||
userNamespace, | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because of microsoft/TypeScript#13086. |
||
} else { | ||
addZipFile(archive, srcFile, normalizedDestPath, stat) | ||
} | ||
} | ||
|
||
// `adm-zip` and `require()` expect Unix paths. | ||
|
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aliases
existed before, I've just added a comment to clarify what it's for.