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

Commit 809b206

Browse files
eduardoboucasSkn0ttkodiakhq[bot]
authoredFeb 15, 2022
feat: add pure ESM support to esbuild and default bundlers (#1018)
* feat: add ESM support to default bundler * feat: add ESM support to esbuild * chore: fix tests * refactor: make return type explicit in signature (let's get rid of those "as" conversions) Co-authored-by: Simon Knott <info@simonknott.de> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 0c1c326 commit 809b206

File tree

7 files changed

+128
-44
lines changed

7 files changed

+128
-44
lines changed
 

‎src/feature_flags.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { env } from 'process'
33
export const defaultFlags: Record<string, boolean> = {
44
buildGoSource: Boolean(env.NETLIFY_EXPERIMENTAL_BUILD_GO_SOURCE),
55
buildRustSource: Boolean(env.NETLIFY_EXPERIMENTAL_BUILD_RUST_SOURCE),
6-
defaultEsModulesToEsbuild: Boolean(env.NETLIFY_EXPERIMENTAL_DEFAULT_ES_MODULES_TO_ESBUILD),
76
parseWithEsbuild: false,
87
traceWithNft: false,
8+
zisi_detect_esm: false,
99
zisi_pure_esm: false,
1010
}
1111

‎src/runtimes/node/bundlers/esbuild/bundler.ts

+17-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import { build, Metafile } from '@netlify/esbuild'
44
import { tmpName } from 'tmp-promise'
55

66
import type { FunctionConfig } from '../../../../config.js'
7+
import { FeatureFlags } from '../../../../feature_flags.js'
78
import { getPathWithExtension, safeUnlink } from '../../../../utils/fs.js'
89
import type { RuntimeName } from '../../../runtime.js'
910
import type { NodeBundlerName } from '../index.js'
1011

11-
import { getBundlerTarget } from './bundler_target.js'
12+
import { getBundlerTarget, getModuleFormat } from './bundler_target.js'
1213
import { getDynamicImportsPlugin } from './plugin_dynamic_imports.js'
1314
import { getNativeModulesPlugin } from './plugin_native_modules.js'
1415
import { getNodeBuiltinPlugin } from './plugin_node_builtin.js'
@@ -28,6 +29,7 @@ export const bundleJsFile = async function ({
2829
basePath,
2930
config,
3031
externalModules = [],
32+
featureFlags,
3133
ignoredModules = [],
3234
name,
3335
srcDir,
@@ -37,6 +39,7 @@ export const bundleJsFile = async function ({
3739
basePath?: string
3840
config: FunctionConfig
3941
externalModules: string[]
42+
featureFlags: FeatureFlags
4043
ignoredModules: string[]
4144
name: string
4245
srcDir: string
@@ -84,11 +87,21 @@ export const bundleJsFile = async function ({
8487
// URLs, not paths, so even on Windows they should use forward slashes.
8588
const sourceRoot = targetDirectory.replace(/\\/g, '/')
8689

90+
// Configuring the output format of esbuild. The `includedFiles` array we get
91+
// here contains additional paths to include with the bundle, like the path
92+
// to a `package.json` with {"type": "module"} in case of an ESM function.
93+
const { includedFiles: includedFilesFromModuleDetection, moduleFormat } = await getModuleFormat(
94+
srcDir,
95+
featureFlags,
96+
config.nodeVersion,
97+
)
98+
8799
try {
88100
const { metafile = { inputs: {}, outputs: {} }, warnings } = await build({
89101
bundle: true,
90102
entryPoints: [srcFile],
91103
external,
104+
format: moduleFormat,
92105
logLevel: 'warning',
93106
logLimit: ESBUILD_LOG_LIMIT,
94107
metafile: true,
@@ -108,12 +121,14 @@ export const bundleJsFile = async function ({
108121
})
109122
const inputs = Object.keys(metafile.inputs).map((path) => resolve(path))
110123
const cleanTempFiles = getCleanupFunction([...bundlePaths.keys()])
124+
const additionalPaths = [...dynamicImportsIncludedPaths, ...includedFilesFromModuleDetection]
111125

112126
return {
113-
additionalPaths: [...dynamicImportsIncludedPaths],
127+
additionalPaths,
114128
bundlePaths,
115129
cleanTempFiles,
116130
inputs,
131+
moduleFormat,
117132
nativeNodeModules,
118133
nodeModulesWithDynamicImports: [...nodeModulesWithDynamicImports],
119134
warnings,

‎src/runtimes/node/bundlers/esbuild/bundler_target.ts

+29-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
const DEFAULT_VERSION = 'node12'
1+
import { FeatureFlags } from '../../../../feature_flags'
2+
import { ModuleFormat } from '../../utils/module_format'
3+
import { DEFAULT_NODE_VERSION, getNodeSupportMatrix } from '../../utils/node_version'
4+
import { getClosestPackageJson } from '../../utils/package_json'
25

36
const versionMap = {
47
'8.x': 'node8',
@@ -10,18 +13,41 @@ const versionMap = {
1013
type VersionKeys = keyof typeof versionMap
1114
type VersionValues = typeof versionMap[VersionKeys]
1215

13-
export const getBundlerTarget = (suppliedVersion?: string): VersionValues => {
16+
const getBundlerTarget = (suppliedVersion?: string): VersionValues => {
1417
const version = normalizeVersion(suppliedVersion)
1518

1619
if (version && version in versionMap) {
1720
return versionMap[version as VersionKeys]
1821
}
1922

20-
return DEFAULT_VERSION
23+
return versionMap[`${DEFAULT_NODE_VERSION}.x`]
24+
}
25+
26+
const getModuleFormat = async (
27+
srcDir: string,
28+
featureFlags: FeatureFlags,
29+
configVersion?: string,
30+
): Promise<{ includedFiles: string[]; moduleFormat: ModuleFormat }> => {
31+
const packageJsonFile = await getClosestPackageJson(srcDir)
32+
const nodeSupport = getNodeSupportMatrix(configVersion)
33+
34+
if (featureFlags.zisi_pure_esm && packageJsonFile?.contents.type === 'module' && nodeSupport.esm) {
35+
return {
36+
includedFiles: [packageJsonFile.path],
37+
moduleFormat: 'esm',
38+
}
39+
}
40+
41+
return {
42+
includedFiles: [],
43+
moduleFormat: 'cjs',
44+
}
2145
}
2246

2347
const normalizeVersion = (version?: string) => {
2448
const match = version && version.match(/^nodejs(.*)$/)
2549

2650
return match ? match[1] : version
2751
}
52+
53+
export { getBundlerTarget, getModuleFormat }

‎src/runtimes/node/bundlers/esbuild/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const bundle: BundleFunction = async ({
6969
bundlePaths,
7070
cleanTempFiles,
7171
inputs,
72+
moduleFormat,
7273
nativeNodeModules = {},
7374
nodeModulesWithDynamicImports,
7475
warnings,
@@ -77,6 +78,7 @@ const bundle: BundleFunction = async ({
7778
basePath,
7879
config,
7980
externalModules,
81+
featureFlags,
8082
ignoredModules,
8183
name,
8284
srcDir,
@@ -122,7 +124,7 @@ const bundle: BundleFunction = async ({
122124
bundlerWarnings,
123125
inputs,
124126
mainFile: normalizedMainFile,
125-
moduleFormat: 'cjs',
127+
moduleFormat,
126128
nativeNodeModules,
127129
nodeModulesWithDynamicImports,
128130
srcFiles: [...supportingSrcFiles, ...bundlePaths.keys()],

‎src/runtimes/node/bundlers/index.ts

+5-7
Original file line numberDiff line numberDiff line change
@@ -101,21 +101,19 @@ export const getDefaultBundler = async ({
101101
mainFile: string
102102
featureFlags: FeatureFlags
103103
}): Promise<NodeBundlerName> => {
104-
const { defaultEsModulesToEsbuild, traceWithNft } = featureFlags
105-
106104
if (['.mjs', '.ts'].includes(extension)) {
107105
return 'esbuild'
108106
}
109107

110-
if (traceWithNft) {
108+
if (featureFlags.traceWithNft) {
111109
return 'nft'
112110
}
113111

114-
if (defaultEsModulesToEsbuild) {
115-
const isEsModule = await detectEsModule({ mainFile })
112+
if (featureFlags.zisi_detect_esm) {
113+
const functionIsESM = await detectEsModule({ mainFile })
116114

117-
if (isEsModule) {
118-
return 'esbuild'
115+
if (functionIsESM) {
116+
return 'nft'
119117
}
120118
}
121119

+56-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { promises as fs } from 'fs'
2+
import { basename, join } from 'path'
23

4+
import findUp from 'find-up'
35
import pkgDir from 'pkg-dir'
46

57
export interface PackageJson {
@@ -16,18 +18,39 @@ export interface PackageJson {
1618
type?: string
1719
}
1820

19-
const sanitiseFiles = (files: unknown): string[] | undefined => {
20-
if (!Array.isArray(files)) {
21-
return undefined
21+
export interface PackageJsonFile {
22+
contents: PackageJson
23+
path: string
24+
}
25+
26+
export const getClosestPackageJson = async (resolveDir: string): Promise<PackageJsonFile | null> => {
27+
const packageJsonPath = await findUp(
28+
async (directory) => {
29+
// We stop traversing if we're about to leave the boundaries of any
30+
// node_modules directory.
31+
if (basename(directory) === 'node_modules') {
32+
return findUp.stop
33+
}
34+
35+
const path = join(directory, 'package.json')
36+
const hasPackageJson = await findUp.exists(path)
37+
38+
return hasPackageJson ? path : undefined
39+
},
40+
{ cwd: resolveDir },
41+
)
42+
43+
if (packageJsonPath === undefined) {
44+
return null
2245
}
2346

24-
return files.filter((file) => typeof file === 'string')
25-
}
47+
const packageJson = await readPackageJson(packageJsonPath)
2648

27-
export const sanitisePackageJson = (packageJson: Record<string, unknown>): PackageJson => ({
28-
...packageJson,
29-
files: sanitiseFiles(packageJson.files),
30-
})
49+
return {
50+
contents: packageJson,
51+
path: packageJsonPath,
52+
}
53+
}
3154

3255
// Retrieve the `package.json` of a specific project or module
3356
export const getPackageJson = async function (srcDir: string): Promise<PackageJson> {
@@ -37,14 +60,7 @@ export const getPackageJson = async function (srcDir: string): Promise<PackageJs
3760
return {}
3861
}
3962

40-
const packageJsonPath = `${packageRoot}/package.json`
41-
try {
42-
// The path depends on the user's build, i.e. must be dynamic
43-
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'))
44-
return sanitisePackageJson(packageJson)
45-
} catch (error) {
46-
throw new Error(`${packageJsonPath} is invalid JSON: ${error.message}`)
47-
}
63+
return readPackageJson(`${packageRoot}/package.json`)
4864
}
4965

5066
export const getPackageJsonIfAvailable = async (srcDir: string): Promise<PackageJson> => {
@@ -56,3 +72,26 @@ export const getPackageJsonIfAvailable = async (srcDir: string): Promise<Package
5672
return {}
5773
}
5874
}
75+
76+
const readPackageJson = async (path: string) => {
77+
try {
78+
// The path depends on the user's build, i.e. must be dynamic
79+
const packageJson = JSON.parse(await fs.readFile(path, 'utf8'))
80+
return sanitisePackageJson(packageJson)
81+
} catch (error) {
82+
throw new Error(`${path} is invalid JSON: ${error.message}`)
83+
}
84+
}
85+
86+
const sanitiseFiles = (files: unknown): string[] | undefined => {
87+
if (!Array.isArray(files)) {
88+
return undefined
89+
}
90+
91+
return files.filter((file) => typeof file === 'string')
92+
}
93+
94+
export const sanitisePackageJson = (packageJson: Record<string, unknown>): PackageJson => ({
95+
...packageJson,
96+
files: sanitiseFiles(packageJson.files),
97+
})

‎tests/main.js

+17-13
Original file line numberDiff line numberDiff line change
@@ -431,18 +431,18 @@ testMany(
431431

432432
testMany(
433433
'Can bundle ESM functions and transpile them to CJS when the Node version is <14',
434-
['bundler_nft'],
434+
['bundler_default', 'bundler_esbuild', 'bundler_nft'],
435435
async (options, t) => {
436436
const length = 4
437437
const fixtureName = 'local-require-esm'
438438
const opts = merge(options, {
439-
basePath: `${FIXTURES_DIR}/${fixtureName}`,
439+
basePath: join(FIXTURES_DIR, fixtureName),
440440
config: {
441441
'*': {
442442
nodeVersion: 'nodejs12.x',
443443
},
444444
},
445-
featureFlags: { defaultEsModulesToEsbuild: false },
445+
featureFlags: { zisi_detect_esm: true, zisi_pure_esm: false },
446446
})
447447
const { files, tmpDir } = await zipFixture(t, fixtureName, {
448448
length,
@@ -485,19 +485,19 @@ testMany(
485485

486486
testMany(
487487
'Can bundle ESM functions and transpile them to CJS when the Node version is <14 and `archiveType` is `none`',
488-
['bundler_esbuild', 'bundler_nft'],
488+
['bundler_default', 'bundler_esbuild', 'bundler_nft'],
489489
async (options, t) => {
490490
const length = 4
491491
const fixtureName = 'local-require-esm'
492492
const opts = merge(options, {
493493
archiveFormat: 'none',
494-
basePath: `${FIXTURES_DIR}/${fixtureName}`,
494+
basePath: join(FIXTURES_DIR, fixtureName),
495495
config: {
496496
'*': {
497497
nodeVersion: 'nodejs12.x',
498498
},
499499
},
500-
featureFlags: { defaultEsModulesToEsbuild: false },
500+
featureFlags: { zisi_detect_esm: true, zisi_pure_esm: false },
501501
})
502502
const { tmpDir } = await zipFixture(t, fixtureName, {
503503
length,
@@ -538,11 +538,14 @@ testMany(
538538

539539
testMany(
540540
'Can bundle CJS functions that import ESM files with an `import()` expression',
541-
['bundler_esbuild', 'bundler_nft'],
541+
['bundler_default', 'bundler_esbuild', 'bundler_nft'],
542542
async (options, t) => {
543543
const fixtureName = 'node-cjs-importing-mjs'
544+
const opts = merge(options, {
545+
featureFlags: { zisi_detect_esm: true },
546+
})
544547
const { files, tmpDir } = await zipFixture(t, fixtureName, {
545-
opts: options,
548+
opts,
546549
})
547550

548551
await unzipFiles(files)
@@ -561,13 +564,13 @@ testMany(
561564

562565
testMany(
563566
'Can bundle native ESM functions when the Node version is >=14 and the `zisi_pure_esm` flag is on',
564-
['bundler_nft'],
567+
['bundler_default', 'bundler_nft', 'bundler_esbuild'],
565568
async (options, t) => {
566569
const length = 2
567570
const fixtureName = 'node-esm'
568571
const opts = merge(options, {
569-
basePath: `${FIXTURES_DIR}/${fixtureName}`,
570-
featureFlags: { zisi_pure_esm: true },
572+
basePath: join(FIXTURES_DIR, fixtureName),
573+
featureFlags: { zisi_detect_esm: true, zisi_pure_esm: true },
571574
})
572575
const { files, tmpDir } = await zipFixture(t, fixtureName, {
573576
length,
@@ -593,12 +596,13 @@ testMany(
593596

594597
testMany(
595598
'Can bundle ESM functions and transpile them to CJS when the Node version is >=14 and the `zisi_pure_esm` flag is off',
596-
['bundler_nft'],
599+
['bundler_default', 'bundler_esbuild', 'bundler_nft'],
597600
async (options, t) => {
598601
const length = 2
599602
const fixtureName = 'node-esm'
600603
const opts = merge(options, {
601-
basePath: `${FIXTURES_DIR}/${fixtureName}`,
604+
basePath: join(FIXTURES_DIR, fixtureName),
605+
featureFlags: { zisi_detect_esm: true },
602606
})
603607
const { files, tmpDir } = await zipFixture(t, fixtureName, {
604608
length,

1 commit comments

Comments
 (1)

github-actions[bot] commented on Feb 15, 2022

@github-actions[bot]
Contributor

⏱ Benchmark results

largeDepsEsbuild: 7.1s

largeDepsZisi: 57.1s

This repository has been archived.