From 21dc4ae4ce341f4679eec945d72567f33444d060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20Bou=C3=A7as?= Date: Mon, 5 Sep 2022 17:05:09 +0100 Subject: [PATCH] feat!: output pure ESM for `.mjs` files (netlify/zip-it-and-ship-it#1198) * feat: output pure ESM for .mjs files * chore: add comment to test * refactor: simplify `outExtension` generation * chore: update test * refactor: remove unused import --- packages/zip-it-and-ship-it/.eslintrc.cjs | 1 + .../zip-it-and-ship-it/src/feature_flags.ts | 14 ++++++++ .../runtimes/node/bundlers/esbuild/bundler.ts | 24 +++++++++++--- .../node/bundlers/esbuild/bundler_target.ts | 10 +++++- .../runtimes/node/bundlers/esbuild/index.ts | 4 ++- .../src/runtimes/node/bundlers/index.ts | 7 +++- .../runtimes/node/bundlers/nft/es_modules.ts | 14 ++++++-- .../runtimes/node/bundlers/zisi/src_files.ts | 2 -- .../src/runtimes/node/index.ts | 1 + .../src/runtimes/node/parser/index.ts | 2 -- .../src/runtimes/node/utils/entry_file.ts | 32 ++++-------------- .../src/runtimes/node/utils/module_format.ts | 19 +++++++++++ .../src/runtimes/node/utils/zip.ts | 25 +++++++++----- .../fixtures/node-mjs-extension/func1.mjs | 4 ++- .../node-mjs-extension/func2/func2.mjs | 4 ++- .../node-mjs-extension/func3/index.mjs | 4 ++- .../node_modules/some-module/index.js | 1 + .../node_modules/some-module/package.json | 4 +++ packages/zip-it-and-ship-it/tests/main.js | 33 +++++++++++++++++++ 19 files changed, 154 insertions(+), 51 deletions(-) create mode 100644 packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/node_modules/some-module/index.js create mode 100644 packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/node_modules/some-module/package.json diff --git a/packages/zip-it-and-ship-it/.eslintrc.cjs b/packages/zip-it-and-ship-it/.eslintrc.cjs index a0c89fbb1b..f5fcb83da0 100644 --- a/packages/zip-it-and-ship-it/.eslintrc.cjs +++ b/packages/zip-it-and-ship-it/.eslintrc.cjs @@ -31,6 +31,7 @@ module.exports = { 'import/extensions': 'off', 'import/no-namespace': 'off', // https://github.com/typescript-eslint/typescript-eslint/issues/2483 + 'max-lines': 'off', 'no-shadow': 'off', '@typescript-eslint/no-shadow': 'error', }, diff --git a/packages/zip-it-and-ship-it/src/feature_flags.ts b/packages/zip-it-and-ship-it/src/feature_flags.ts index 1f66236972..30c3530cc0 100644 --- a/packages/zip-it-and-ship-it/src/feature_flags.ts +++ b/packages/zip-it-and-ship-it/src/feature_flags.ts @@ -1,10 +1,24 @@ import { env } from 'process' export const defaultFlags: Record = { + // Build Rust functions from source. buildRustSource: Boolean(env.NETLIFY_EXPERIMENTAL_BUILD_RUST_SOURCE), + + // Use esbuild to trace dependencies in the legacy bundler. parseWithEsbuild: false, + + // Use NFT as the default bundler. traceWithNft: false, + + // Output pure (i.e. untranspiled) ESM files when the function file has ESM + // syntax and the parent `package.json` file has `{"type": "module"}`. zisi_pure_esm: false, + + // Output pure (i.e. untranspiled) ESM files when the function file has a + // `.mjs` extension. + zisi_pure_esm_mjs: false, + + // Load configuration from per-function JSON files. project_deploy_configuration_api_use_per_function_configuration_files: false, } diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/bundler.ts b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/bundler.ts index 54aa7c8536..1cdb963ccf 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/bundler.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/bundler.ts @@ -8,6 +8,7 @@ import { FeatureFlags } from '../../../../feature_flags.js' import { FunctionBundlingUserError } from '../../../../utils/error.js' import { getPathWithExtension, safeUnlink } from '../../../../utils/fs.js' import { RuntimeType } from '../../../runtime.js' +import { getFileExtensionForFormat, ModuleFileExtension, ModuleFormat } from '../../utils/module_format.js' import { NodeBundlerType } from '../types.js' import { getBundlerTarget, getModuleFormat } from './bundler_target.js' @@ -32,6 +33,7 @@ export const bundleJsFile = async function ({ externalModules = [], featureFlags, ignoredModules = [], + mainFile, name, srcDir, srcFile, @@ -42,6 +44,7 @@ export const bundleJsFile = async function ({ externalModules: string[] featureFlags: FeatureFlags ignoredModules: string[] + mainFile: string name: string srcDir: string srcFile: string @@ -94,9 +97,17 @@ export const bundleJsFile = async function ({ const { includedFiles: includedFilesFromModuleDetection, moduleFormat } = await getModuleFormat( srcDir, featureFlags, + extname(mainFile), config.nodeVersion, ) + // The extension of the output file. + const extension = getFileExtensionForFormat(moduleFormat, featureFlags) + + // When outputting an ESM file, configure esbuild to produce a `.mjs` file. + const outExtension = + moduleFormat === ModuleFormat.ESM ? { [ModuleFileExtension.JS]: ModuleFileExtension.MJS } : undefined + try { const { metafile = { inputs: {}, outputs: {} }, warnings } = await build({ bundle: true, @@ -107,6 +118,7 @@ export const bundleJsFile = async function ({ logLimit: ESBUILD_LOG_LIMIT, metafile: true, nodePaths: additionalModulePaths, + outExtension, outdir: targetDirectory, platform: 'node', plugins, @@ -117,6 +129,7 @@ export const bundleJsFile = async function ({ }) const bundlePaths = getBundlePaths({ destFolder: targetDirectory, + extension, outputs: metafile.outputs, srcFile, }) @@ -128,6 +141,7 @@ export const bundleJsFile = async function ({ additionalPaths, bundlePaths, cleanTempFiles, + extension, inputs, moduleFormat, nativeNodeModules, @@ -149,14 +163,16 @@ export const bundleJsFile = async function ({ // with the `aliases` format used upstream. const getBundlePaths = ({ destFolder, + extension: outputExtension, outputs, srcFile, }: { destFolder: string + extension: string outputs: Metafile['outputs'] srcFile: string }) => { - const bundleFilename = `${basename(srcFile, extname(srcFile))}.js` + const bundleFilename = basename(srcFile, extname(srcFile)) + outputExtension const mainFileDirectory = dirname(srcFile) const bundlePaths: Map = new Map() @@ -171,11 +187,11 @@ const getBundlePaths = ({ const absolutePath = join(destFolder, filename) if (output.entryPoint && basename(output.entryPoint) === basename(srcFile)) { - // Ensuring the main file has a `.js` extension. - const normalizedSrcFile = getPathWithExtension(srcFile, '.js') + // Ensuring the main file has the right extension. + const normalizedSrcFile = getPathWithExtension(srcFile, outputExtension) bundlePaths.set(absolutePath, normalizedSrcFile) - } else if (extension === '.js' || filename === `${bundleFilename}.map`) { + } else if (extension === outputExtension || filename === `${bundleFilename}.map`) { bundlePaths.set(absolutePath, join(mainFileDirectory, filename)) } }) diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/bundler_target.ts b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/bundler_target.ts index 37e712b2fb..0a572cdc05 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/bundler_target.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/bundler_target.ts @@ -1,5 +1,5 @@ import { FeatureFlags } from '../../../../feature_flags' -import { ModuleFormat } from '../../utils/module_format' +import { ModuleFileExtension, ModuleFormat } from '../../utils/module_format' import { DEFAULT_NODE_VERSION, getNodeSupportMatrix, @@ -31,8 +31,16 @@ const getBundlerTarget = (suppliedVersion?: NodeVersionString): VersionValues => const getModuleFormat = async ( srcDir: string, featureFlags: FeatureFlags, + extension: string, configVersion?: string, ): Promise<{ includedFiles: string[]; moduleFormat: ModuleFormat }> => { + if (extension === ModuleFileExtension.MJS && featureFlags.zisi_pure_esm_mjs) { + return { + includedFiles: [], + moduleFormat: ModuleFormat.ESM, + } + } + const packageJsonFile = await getClosestPackageJson(srcDir) const nodeSupport = getNodeSupportMatrix(configVersion) diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/index.ts b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/index.ts index 5f658ccad3..6f69207186 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/index.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/esbuild/index.ts @@ -68,6 +68,7 @@ const bundle: BundleFunction = async ({ additionalPaths, bundlePaths, cleanTempFiles, + extension: outputExtension, inputs, moduleFormat, nativeNodeModules = {}, @@ -80,6 +81,7 @@ const bundle: BundleFunction = async ({ externalModules, featureFlags, ignoredModules, + mainFile, name, srcDir, srcFile: mainFile, @@ -109,7 +111,7 @@ const bundle: BundleFunction = async ({ // path of the original, pre-bundling function file. We'll add the actual // bundled file further below. const supportingSrcFiles = srcFiles.filter((path) => path !== mainFile) - const normalizedMainFile = getPathWithExtension(mainFile, '.js') + const normalizedMainFile = getPathWithExtension(mainFile, outputExtension) const functionBasePath = getFunctionBasePath({ basePathFromConfig: basePath, mainFile, diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/index.ts b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/index.ts index 7845b5dd6e..26a48a1ba2 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/index.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/index.ts @@ -3,6 +3,7 @@ import { extname } from 'path' import { FunctionConfig } from '../../../config.js' import { FeatureFlags } from '../../../feature_flags.js' import { detectEsModule } from '../utils/detect_es_module.js' +import { ModuleFileExtension } from '../utils/module_format.js' import esbuildBundler from './esbuild/index.js' import nftBundler from './nft/index.js' @@ -61,6 +62,10 @@ const getDefaultBundler = async ({ mainFile: string featureFlags: FeatureFlags }): Promise => { + if (extension === ModuleFileExtension.MJS && featureFlags.zisi_pure_esm_mjs) { + return NodeBundlerType.NFT + } + if (ESBUILD_EXTENSIONS.has(extension)) { return NodeBundlerType.ESBUILD } @@ -69,7 +74,7 @@ const getDefaultBundler = async ({ return NodeBundlerType.NFT } - const functionIsESM = extname(mainFile) !== '.cjs' && (await detectEsModule({ mainFile })) + const functionIsESM = extname(mainFile) !== ModuleFileExtension.CJS && (await detectEsModule({ mainFile })) return functionIsESM ? NodeBundlerType.NFT : NodeBundlerType.ZISI } diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/nft/es_modules.ts b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/nft/es_modules.ts index e2b38b791f..e656a6b060 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/nft/es_modules.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/nft/es_modules.ts @@ -1,11 +1,11 @@ -import { basename, dirname, resolve } from 'path' +import { basename, dirname, extname, resolve } from 'path' import { NodeFileTraceReasons } from '@vercel/nft' import type { FunctionConfig } from '../../../../config.js' import { FeatureFlags } from '../../../../feature_flags.js' import { cachedReadFile, FsCache } from '../../../../utils/fs.js' -import { ModuleFormat } from '../../utils/module_format.js' +import { ModuleFileExtension, ModuleFormat } from '../../utils/module_format.js' import { getNodeSupportMatrix } from '../../utils/node_version.js' import { getPackageJsonIfAvailable, PackageJson } from '../../utils/package_json.js' @@ -67,6 +67,16 @@ export const processESM = async ({ reasons: NodeFileTraceReasons name: string }): Promise<{ rewrites?: Map; moduleFormat: ModuleFormat }> => { + const extension = extname(mainFile) + + // If this is a .mjs file and we want to output pure ESM files, we don't need + // to transpile anything. + if (extension === ModuleFileExtension.MJS && featureFlags.zisi_pure_esm_mjs) { + return { + moduleFormat: ModuleFormat.ESM, + } + } + const entrypointIsESM = isEntrypointESM({ basePath, esmPaths, mainFile }) if (!entrypointIsESM) { diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/zisi/src_files.ts b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/zisi/src_files.ts index 551a8453cc..aea1d13aa8 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/zisi/src_files.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/bundlers/zisi/src_files.ts @@ -1,4 +1,3 @@ -/* eslint-disable max-lines */ import { dirname, basename, normalize } from 'path' import { not as notJunk } from 'junk' @@ -208,4 +207,3 @@ const getTreeShakedDependencies = async function ({ return [path, ...depsPath] } -/* eslint-enable max-lines */ diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/index.ts b/packages/zip-it-and-ship-it/src/runtimes/node/index.ts index 49bbff4bc9..1f9deb5774 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/index.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/index.ts @@ -103,6 +103,7 @@ const zipFunction: ZipFunction = async function ({ basePath: finalBasePath, destFolder, extension, + featureFlags, filename, mainFile: finalMainFile, moduleFormat, diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/parser/index.ts b/packages/zip-it-and-ship-it/src/runtimes/node/parser/index.ts index 7d5b900368..780feea0c0 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/parser/index.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/parser/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable max-lines */ import { promises as fs } from 'fs' import { join, relative, resolve } from 'path' @@ -225,4 +224,3 @@ const validateGlobNodes = (globNodes: string[]) => { return hasStrings && hasStaticHead } -/* eslint-enable max-lines */ diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/utils/entry_file.ts b/packages/zip-it-and-ship-it/src/runtimes/node/utils/entry_file.ts index 1e25a62d32..3d38832772 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/utils/entry_file.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/utils/entry_file.ts @@ -1,43 +1,23 @@ -import { basename, extname } from 'path' - import { ModuleFormat } from './module_format.js' import { normalizeFilePath } from './normalize_path.js' -export interface EntryFile { - contents: string - filename: string -} - -const getEntryFileContents = (mainPath: string, moduleFormat: string) => { - const importPath = `.${mainPath.startsWith('/') ? mainPath : `/${mainPath}`}` - - if (moduleFormat === ModuleFormat.COMMONJS) { - return `module.exports = require('${importPath}')` - } - - return `export { handler } from '${importPath}'` -} - export const getEntryFile = ({ commonPrefix, - filename, mainFile, moduleFormat, userNamespace, }: { commonPrefix: string - filename: string mainFile: string moduleFormat: ModuleFormat userNamespace: string -}): EntryFile => { +}) => { const mainPath = normalizeFilePath({ commonPrefix, path: mainFile, userNamespace }) - const extension = extname(filename) - const entryFilename = `${basename(filename, extension)}.js` - const contents = getEntryFileContents(mainPath, moduleFormat) + const importPath = `.${mainPath.startsWith('/') ? mainPath : `/${mainPath}`}` - return { - contents, - filename: entryFilename, + if (moduleFormat === ModuleFormat.COMMONJS) { + return `module.exports = require('${importPath}')` } + + return `export { handler } from '${importPath}'` } diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/utils/module_format.ts b/packages/zip-it-and-ship-it/src/runtimes/node/utils/module_format.ts index 1b68f484b1..70a20d273e 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/utils/module_format.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/utils/module_format.ts @@ -1,4 +1,23 @@ +import type { FeatureFlags } from '../../../feature_flags.js' + export const enum ModuleFormat { COMMONJS = 'cjs', ESM = 'esm', } + +export const enum ModuleFileExtension { + CJS = '.cjs', + JS = '.js', + MJS = '.mjs', +} + +export const getFileExtensionForFormat = ( + moduleFormat: ModuleFormat, + featureFlags: FeatureFlags, +): ModuleFileExtension => { + if (moduleFormat === ModuleFormat.ESM && featureFlags.zisi_pure_esm_mjs) { + return ModuleFileExtension.MJS + } + + return ModuleFileExtension.JS +} diff --git a/packages/zip-it-and-ship-it/src/runtimes/node/utils/zip.ts b/packages/zip-it-and-ship-it/src/runtimes/node/utils/zip.ts index 0ed6275f57..a2ee105c9e 100644 --- a/packages/zip-it-and-ship-it/src/runtimes/node/utils/zip.ts +++ b/packages/zip-it-and-ship-it/src/runtimes/node/utils/zip.ts @@ -8,10 +8,11 @@ import deleteFiles from 'del' import pMap from 'p-map' import { startZip, addZipFile, addZipContent, endZip, ZipArchive } from '../../../archive.js' +import type { FeatureFlags } from '../../../feature_flags.js' import { mkdirAndWriteFile } from '../../../utils/fs.js' -import { EntryFile, getEntryFile } from './entry_file.js' -import type { ModuleFormat } from './module_format.js' +import { getEntryFile } from './entry_file.js' +import { getFileExtensionForFormat, ModuleFormat } from './module_format.js' import { normalizeFilePath } from './normalize_path.js' // Taken from https://www.npmjs.com/package/cpy. @@ -28,6 +29,7 @@ interface ZipNodeParameters { basePath: string destFolder: string extension: string + featureFlags: FeatureFlags filename: string mainFile: string moduleFormat: ModuleFormat @@ -40,27 +42,30 @@ const createDirectory = async function ({ basePath, destFolder, extension, + featureFlags, filename, mainFile, moduleFormat, rewrites = new Map(), srcFiles, }: ZipNodeParameters) { - const { contents: entryContents, filename: entryFilename } = getEntryFile({ + const entryFile = getEntryFile({ commonPrefix: basePath, - filename, mainFile, moduleFormat, userNamespace: DEFAULT_USER_SUBDIRECTORY, }) + const entryFileExtension = getFileExtensionForFormat(moduleFormat, featureFlags) + const entryFilename = basename(filename, extension) + entryFileExtension const functionFolder = join(destFolder, basename(filename, extension)) + const entryFilePath = resolve(functionFolder, entryFilename) // Deleting the functions directory in case it exists before creating it. await deleteFiles(functionFolder, { force: true }) await fs.mkdir(functionFolder, { recursive: true }) // Writing entry file. - await fs.writeFile(join(functionFolder, entryFilename), entryContents) + await fs.writeFile(entryFilePath, entryFile) // Copying source files. await pMap( @@ -91,6 +96,7 @@ const createZipArchive = async function ({ basePath, destFolder, extension, + featureFlags, filename, mainFile, moduleFormat, @@ -99,7 +105,8 @@ const createZipArchive = async function ({ }: ZipNodeParameters) { const destPath = join(destFolder, `${basename(filename, extension)}.zip`) const { archive, output } = startZip(destPath) - const entryFilename = `${basename(filename, extension)}.js` + const entryFileExtension = getFileExtensionForFormat(moduleFormat, featureFlags) + const entryFilename = basename(filename, extension) + entryFileExtension const entryFilePath = resolve(basePath, entryFilename) // We don't need an entry file if it would end up with the same path as the @@ -116,9 +123,9 @@ const createZipArchive = async function ({ const userNamespace = hasEntryFileConflict ? DEFAULT_USER_SUBDIRECTORY : '' if (needsEntryFile) { - const entryFile = getEntryFile({ commonPrefix: basePath, filename, mainFile, moduleFormat, userNamespace }) + const entryFile = getEntryFile({ commonPrefix: basePath, mainFile, moduleFormat, userNamespace }) - addEntryFileToZip(archive, entryFile) + addEntryFileToZip(archive, entryFile, basename(entryFilePath)) } const srcFilesInfos = await Promise.all(srcFiles.map(addStat)) @@ -153,7 +160,7 @@ export const zipNodeJs = function ({ return createDirectory(options) } -const addEntryFileToZip = function (archive: ZipArchive, { contents, filename }: EntryFile) { +const addEntryFileToZip = function (archive: ZipArchive, contents: string, filename: string) { const contentBuffer = Buffer.from(contents) addZipContent(archive, contentBuffer, filename) diff --git a/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func1.mjs b/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func1.mjs index 767df32b9c..1d76e90c92 100644 --- a/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func1.mjs +++ b/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func1.mjs @@ -1 +1,3 @@ -export const handler = () => true +import { format } from 'some-module' + +export const handler = () => format === 'esm' diff --git a/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func2/func2.mjs b/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func2/func2.mjs index 767df32b9c..1d76e90c92 100644 --- a/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func2/func2.mjs +++ b/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func2/func2.mjs @@ -1 +1,3 @@ -export const handler = () => true +import { format } from 'some-module' + +export const handler = () => format === 'esm' diff --git a/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func3/index.mjs b/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func3/index.mjs index 767df32b9c..1d76e90c92 100644 --- a/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func3/index.mjs +++ b/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/func3/index.mjs @@ -1 +1,3 @@ -export const handler = () => true +import { format } from 'some-module' + +export const handler = () => format === 'esm' diff --git a/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/node_modules/some-module/index.js b/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/node_modules/some-module/index.js new file mode 100644 index 0000000000..b1797e8b6c --- /dev/null +++ b/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/node_modules/some-module/index.js @@ -0,0 +1 @@ +export const format = 'esm' diff --git a/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/node_modules/some-module/package.json b/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/node_modules/some-module/package.json new file mode 100644 index 0000000000..ab378bad72 --- /dev/null +++ b/packages/zip-it-and-ship-it/tests/fixtures/node-mjs-extension/node_modules/some-module/package.json @@ -0,0 +1,4 @@ +{ + "main": "index.js", + "type": "module" +} diff --git a/packages/zip-it-and-ship-it/tests/main.js b/packages/zip-it-and-ship-it/tests/main.js index f0d9b41a81..8e07ec3a25 100644 --- a/packages/zip-it-and-ship-it/tests/main.js +++ b/packages/zip-it-and-ship-it/tests/main.js @@ -2890,3 +2890,36 @@ testMany('None bundler emits esm with default nodeVersion', ['bundler_none'], as t.is(originalFile, bundledFile) }) + +testMany( + 'Can bundle native ESM functions when the extension is `.mjs` and the `zisi_pure_esm_mjs` feature flag is on', + allBundleConfigs, + async (options, t) => { + const length = 3 + const fixtureName = 'node-mjs-extension' + const opts = merge(options, { + basePath: join(FIXTURES_DIR, fixtureName), + featureFlags: { zisi_pure_esm_mjs: true }, + }) + const { files, tmpDir } = await zipFixture(t, fixtureName, { + length, + opts, + }) + + await unzipFiles(files, (path) => `${path}/../${basename(path)}_out`) + + for (let index = 1; index <= length; index++) { + const funcDir = join(tmpDir, `func${index}.zip_out`) + + // Writing a basic package.json with `type: "module"` just so that we can + // import the functions from the test. + await writeFile(join(funcDir, 'package.json'), JSON.stringify({ type: 'module' })) + + const funcFile = join(funcDir, `func${index}.mjs`) + const func = await importFunctionFile(funcFile) + + t.true(func.handler()) + t.true(await detectEsModule({ mainFile: funcFile })) + } + }, +)