Skip to content

Commit

Permalink
feat!: output pure ESM for .mjs files (netlify/zip-it-and-ship-it#1198
Browse files Browse the repository at this point in the history
)

* feat: output pure ESM for .mjs files

* chore: add comment to test

* refactor: simplify `outExtension` generation

* chore: update test

* refactor: remove unused import
  • Loading branch information
eduardoboucas authored Sep 5, 2022
1 parent 132de91 commit 21dc4ae
Show file tree
Hide file tree
Showing 19 changed files with 154 additions and 51 deletions.
1 change: 1 addition & 0 deletions packages/zip-it-and-ship-it/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
14 changes: 14 additions & 0 deletions packages/zip-it-and-ship-it/src/feature_flags.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import { env } from 'process'

export const defaultFlags: Record<string, boolean> = {
// 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,
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -32,6 +33,7 @@ export const bundleJsFile = async function ({
externalModules = [],
featureFlags,
ignoredModules = [],
mainFile,
name,
srcDir,
srcFile,
Expand All @@ -42,6 +44,7 @@ export const bundleJsFile = async function ({
externalModules: string[]
featureFlags: FeatureFlags
ignoredModules: string[]
mainFile: string
name: string
srcDir: string
srcFile: string
Expand Down Expand Up @@ -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,
Expand All @@ -107,6 +118,7 @@ export const bundleJsFile = async function ({
logLimit: ESBUILD_LOG_LIMIT,
metafile: true,
nodePaths: additionalModulePaths,
outExtension,
outdir: targetDirectory,
platform: 'node',
plugins,
Expand All @@ -117,6 +129,7 @@ export const bundleJsFile = async function ({
})
const bundlePaths = getBundlePaths({
destFolder: targetDirectory,
extension,
outputs: metafile.outputs,
srcFile,
})
Expand All @@ -128,6 +141,7 @@ export const bundleJsFile = async function ({
additionalPaths,
bundlePaths,
cleanTempFiles,
extension,
inputs,
moduleFormat,
nativeNodeModules,
Expand All @@ -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<string, string> = new Map()

Expand All @@ -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))
}
})
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const bundle: BundleFunction = async ({
additionalPaths,
bundlePaths,
cleanTempFiles,
extension: outputExtension,
inputs,
moduleFormat,
nativeNodeModules = {},
Expand All @@ -80,6 +81,7 @@ const bundle: BundleFunction = async ({
externalModules,
featureFlags,
ignoredModules,
mainFile,
name,
srcDir,
srcFile: mainFile,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -61,6 +62,10 @@ const getDefaultBundler = async ({
mainFile: string
featureFlags: FeatureFlags
}): Promise<NodeBundlerType> => {
if (extension === ModuleFileExtension.MJS && featureFlags.zisi_pure_esm_mjs) {
return NodeBundlerType.NFT
}

if (ESBUILD_EXTENSIONS.has(extension)) {
return NodeBundlerType.ESBUILD
}
Expand All @@ -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
}
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -67,6 +67,16 @@ export const processESM = async ({
reasons: NodeFileTraceReasons
name: string
}): Promise<{ rewrites?: Map<string, string>; 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable max-lines */
import { dirname, basename, normalize } from 'path'

import { not as notJunk } from 'junk'
Expand Down Expand Up @@ -208,4 +207,3 @@ const getTreeShakedDependencies = async function ({

return [path, ...depsPath]
}
/* eslint-enable max-lines */
1 change: 1 addition & 0 deletions packages/zip-it-and-ship-it/src/runtimes/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const zipFunction: ZipFunction = async function ({
basePath: finalBasePath,
destFolder,
extension,
featureFlags,
filename,
mainFile: finalMainFile,
moduleFormat,
Expand Down
2 changes: 0 additions & 2 deletions packages/zip-it-and-ship-it/src/runtimes/node/parser/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* eslint-disable max-lines */
import { promises as fs } from 'fs'
import { join, relative, resolve } from 'path'

Expand Down Expand Up @@ -225,4 +224,3 @@ const validateGlobNodes = (globNodes: string[]) => {

return hasStrings && hasStaticHead
}
/* eslint-enable max-lines */
32 changes: 6 additions & 26 deletions packages/zip-it-and-ship-it/src/runtimes/node/utils/entry_file.ts
Original file line number Diff line number Diff line change
@@ -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}'`
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 21dc4ae

Please sign in to comment.