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

Commit

Permalink
fix: output mjs and ts v2 functions with mjs extension (#1505)
Browse files Browse the repository at this point in the history
* fix: output mjs and ts v2 functions with mjs extension

* feat: use right extension for TS files

* chore: formatting

* chore: update paths in tests

* chore: update paths in test

* refactor: rename variable

* refactor: remove unnecessary check

* chore: update test

* chore: rename variable

* feat: ensure `package.json` with `type: "module"`

* refactor: move logic to ZIP generator

* refactor: reuse common method

* chore: restructure code

* feat: read tsconfig

* chore: hello

* refactor: revert abstraction

* chore: add comment

* chore: update vitest config

* refactor: revert default value

* fix: account for CJS and ESM entry files

* feat: plant `package.json`

* chore: add placeholder tsconfig

* refactor: remove check for ESM

* fix: emit ESM for `es6` and `es2015`

* feat: set boundary to `tsconfig.json`

* feat: add boundary for `package.json` search

---------

Co-authored-by: Eduardo Bouças <mail@eduardoboucas.com>
  • Loading branch information
danez and eduardoboucas committed Aug 3, 2023
1 parent 9d3cc9a commit 4bc75ba
Show file tree
Hide file tree
Showing 27 changed files with 211 additions and 89 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,5 @@ module.exports = {
},
},
],
ignorePatterns: ['tests/fixtures/**/*'],
ignorePatterns: ['tests/fixtures/**/*', 'tests/fixtures-esm/**/*'],
}
7 changes: 2 additions & 5 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"execa": "^6.0.0",
"filter-obj": "^5.0.0",
"find-up": "^6.0.0",
"get-tsconfig": "^4.6.2",
"glob": "^8.0.3",
"is-builtin-module": "^3.1.0",
"is-path-inside": "^4.0.0",
Expand Down
16 changes: 8 additions & 8 deletions src/runtimes/node/bundlers/esbuild/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,18 +107,18 @@ export const bundleJsFile = async function ({
})

// The extension of the output file.
const outputExtension = getFileExtensionForFormat(moduleFormat, featureFlags)
const outputExtension = getFileExtensionForFormat(moduleFormat, featureFlags, runtimeAPIVersion)

// We add this banner so that calls to require() still work in ESM modules, especially when importing node built-ins
// We have to do this until this is fixed in esbuild: https://github.com/evanw/esbuild/pull/2067
const esmJSBanner = `
import {createRequire as ___nfyCreateRequire} from "module";
import {fileURLToPath as ___nfyFileURLToPath} from "url";
import {dirname as ___nfyPathDirname} from "path";
let __filename=___nfyFileURLToPath(import.meta.url);
let __dirname=___nfyPathDirname(___nfyFileURLToPath(import.meta.url));
let require=___nfyCreateRequire(import.meta.url);
`
import {createRequire as ___nfyCreateRequire} from "module";
import {fileURLToPath as ___nfyFileURLToPath} from "url";
import {dirname as ___nfyPathDirname} from "path";
let __filename=___nfyFileURLToPath(import.meta.url);
let __dirname=___nfyPathDirname(___nfyFileURLToPath(import.meta.url));
let require=___nfyCreateRequire(import.meta.url);
`

try {
const { metafile = { inputs: {}, outputs: {} }, warnings } = await build({
Expand Down
29 changes: 1 addition & 28 deletions src/runtimes/node/bundlers/nft/es_modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ import { NodeFileTraceReasons } from '@vercel/nft'
import type { FunctionConfig } from '../../../../config.js'
import { FeatureFlags } from '../../../../feature_flags.js'
import type { RuntimeCache } from '../../../../utils/cache.js'
import { FunctionBundlingUserError } from '../../../../utils/error.js'
import { cachedReadFile } from '../../../../utils/fs.js'
import { RUNTIME } from '../../../runtime.js'
import { ModuleFormat, MODULE_FILE_EXTENSION, MODULE_FORMAT } from '../../utils/module_format.js'
import { getNodeSupportMatrix } from '../../utils/node_version.js'
import { getPackageJsonIfAvailable, PackageJson } from '../../utils/package_json.js'
import { NODE_BUNDLER } from '../types.js'

import { transpile } from './transpile.js'

Expand Down Expand Up @@ -83,20 +80,7 @@ export const processESM = async ({
}
}

const entrypointIsESM = isEntrypointESM({ basePath, esmPaths, mainFile })

if (!entrypointIsESM) {
if (runtimeAPIVersion === 2) {
throw new FunctionBundlingUserError(
`The function '${name}' must use the ES module syntax. To learn more, visit https://ntl.fyi/esm.`,
{
functionName: name,
runtime: RUNTIME.JAVASCRIPT,
bundler: NODE_BUNDLER.NFT,
},
)
}

if (!isEntrypointESM({ basePath, esmPaths, mainFile })) {
return {
moduleFormat: MODULE_FORMAT.COMMONJS,
}
Expand All @@ -111,17 +95,6 @@ export const processESM = async ({
}
}

if (runtimeAPIVersion === 2) {
throw new FunctionBundlingUserError(
`The function '${name}' must use the ES module syntax. To learn more, visit https://ntl.fyi/esm.`,
{
functionName: name,
runtime: RUNTIME.JAVASCRIPT,
bundler: NODE_BUNDLER.NFT,
},
)
}

const rewrites = await transpileESM({ basePath, cache, config, esmPaths, reasons, name })

return {
Expand Down
25 changes: 15 additions & 10 deletions src/runtimes/node/bundlers/nft/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { cachedReadFile, getPathWithExtension } from '../../../../utils/fs.js'
import { minimatch } from '../../../../utils/matching.js'
import { getBasePath } from '../../utils/base_path.js'
import { filterExcludedPaths, getPathsOfIncludedFiles } from '../../utils/included_files.js'
import { MODULE_FORMAT } from '../../utils/module_format.js'
import { MODULE_FORMAT, MODULE_FILE_EXTENSION, tsExtensions } from '../../utils/module_format.js'
import { getNodeSupportMatrix } from '../../utils/node_version.js'
import { getModuleFormat as getTSModuleFormat } from '../../utils/tsconfig.js'
import type { GetSrcFilesFunction, BundleFunction } from '../types.js'

import { processESM } from './es_modules.js'
Expand Down Expand Up @@ -49,6 +50,7 @@ const bundle: BundleFunction = async ({
mainFile,
pluginsModulesPath,
name,
repositoryRoot,
runtimeAPIVersion,
})
const includedPaths = filterExcludedPaths(includedFilePaths, excludePatterns)
Expand Down Expand Up @@ -91,6 +93,7 @@ const traceFilesAndTranspile = async function ({
mainFile,
pluginsModulesPath,
name,
repositoryRoot,
runtimeAPIVersion,
}: {
basePath?: string
Expand All @@ -100,9 +103,11 @@ const traceFilesAndTranspile = async function ({
mainFile: string
pluginsModulesPath?: string
name: string
repositoryRoot?: string
runtimeAPIVersion: number
}) {
const isTypeScript = extname(mainFile) === '.ts'
const isTypeScript = tsExtensions.has(extname(mainFile))
const tsFormat = isTypeScript ? getTSModuleFormat(mainFile, repositoryRoot) : MODULE_FORMAT.COMMONJS
const tsAliases = new Map<string, string>()
const tsRewrites = new Map<string, string>()

Expand All @@ -118,9 +123,11 @@ const traceFilesAndTranspile = async function ({
ignore: getIgnoreFunction(config),
readFile: async (path: string) => {
try {
if (extname(path) === '.ts') {
const transpiledSource = await transpile({ config, name, path })
const newPath = getPathWithExtension(path, '.js')
const extension = extname(path)

if (tsExtensions.has(extension)) {
const transpiledSource = await transpile({ config, name, format: tsFormat, path })
const newPath = getPathWithExtension(path, MODULE_FILE_EXTENSION.JS)

// Overriding the contents of the `.ts` file.
tsRewrites.set(path, transpiledSource)
Expand All @@ -131,9 +138,7 @@ const traceFilesAndTranspile = async function ({
return transpiledSource
}

const source = await cachedReadFile(cache.fileCache, path)

return source
return await cachedReadFile(cache.fileCache, path)
} catch (error) {
if (error.code === 'ENOENT' || error.code === 'EISDIR') {
return null
Expand Down Expand Up @@ -166,8 +171,8 @@ const traceFilesAndTranspile = async function ({
if (isTypeScript) {
return {
aliases: tsAliases,
mainFile: getPathWithExtension(mainFile, '.js'),
moduleFormat: MODULE_FORMAT.ESM,
mainFile: getPathWithExtension(mainFile, MODULE_FILE_EXTENSION.JS),
moduleFormat: tsFormat,
paths: normalizedDependencyPaths,
rewrites: tsRewrites,
}
Expand Down
4 changes: 3 additions & 1 deletion src/runtimes/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const zipFunction: ZipFunction = async function ({
moduleFormat,
nativeNodeModules,
nodeModulesWithDynamicImports,
rewrites,
rewrites = new Map(),
srcFiles,
} = await bundler.bundle({
basePath,
Expand Down Expand Up @@ -117,6 +117,8 @@ const zipFunction: ZipFunction = async function ({
filename,
mainFile: finalMainFile,
moduleFormat,
name,
repositoryRoot,
rewrites,
runtimeAPIVersion,
srcFiles,
Expand Down
10 changes: 7 additions & 3 deletions src/runtimes/node/utils/entry_file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ const getEntryFileContents = (
if (runtimeAPIVersion === 2) {
return [
`import * as func from '${importPath}'`,
`import { getLambdaHandler } from './${BOOTSTRAP_FILE_NAME}'`,
`export const handler = getLambdaHandler(func)`,
`import * as bootstrap from './${BOOTSTRAP_FILE_NAME}'`,

// See https://esbuild.github.io/content-types/#default-interop.
'const funcModule = typeof func.default === "function" ? func : func.default',

`export const handler = bootstrap.getLambdaHandler(funcModule)`,
].join(';')
}

Expand Down Expand Up @@ -167,7 +171,7 @@ export const getEntryFile = ({
runtimeAPIVersion: number
}): EntryFile => {
const mainPath = normalizeFilePath({ commonPrefix, path: mainFile, userNamespace })
const extension = getFileExtensionForFormat(moduleFormat, featureFlags)
const extension = getFileExtensionForFormat(moduleFormat, featureFlags, runtimeAPIVersion)
const entryFilename = getEntryFileName({ extension, featureFlags, filename, runtimeAPIVersion })
const contents = getEntryFileContents(mainPath, moduleFormat, featureFlags, runtimeAPIVersion)

Expand Down
5 changes: 4 additions & 1 deletion src/runtimes/node/utils/module_format.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { FeatureFlags } from '../../../feature_flags.js'
import { ObjectValues } from '../../../types/utils.js'

export const tsExtensions = new Set(['.ts', '.cts', '.mts'])

export const MODULE_FORMAT = {
COMMONJS: 'cjs',
ESM: 'esm',
Expand All @@ -19,8 +21,9 @@ export type ModuleFileExtension = ObjectValues<typeof MODULE_FILE_EXTENSION>
export const getFileExtensionForFormat = (
moduleFormat: ModuleFormat,
featureFlags: FeatureFlags,
runtimeAPIVersion: number,
): ModuleFileExtension => {
if (moduleFormat === MODULE_FORMAT.ESM && featureFlags.zisi_pure_esm_mjs) {
if (moduleFormat === MODULE_FORMAT.ESM && (runtimeAPIVersion === 2 || featureFlags.zisi_pure_esm_mjs)) {
return MODULE_FILE_EXTENSION.MJS
}

Expand Down
4 changes: 2 additions & 2 deletions src/runtimes/node/utils/package_json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface PackageJsonFile {
path: string
}

export const getClosestPackageJson = async (resolveDir: string): Promise<PackageJsonFile | null> => {
export const getClosestPackageJson = async (resolveDir: string, boundary?: string): Promise<PackageJsonFile | null> => {
const packageJsonPath = await findUp(
async (directory) => {
// We stop traversing if we're about to leave the boundaries of any
Expand All @@ -36,7 +36,7 @@ export const getClosestPackageJson = async (resolveDir: string): Promise<Package

return hasPackageJson ? path : undefined
},
{ cwd: resolveDir },
{ cwd: resolveDir, stopAt: boundary },
)

if (packageJsonPath === undefined) {
Expand Down
27 changes: 27 additions & 0 deletions src/runtimes/node/utils/tsconfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { dirname, relative } from 'path'

import { getTsconfig } from 'get-tsconfig'

import { MODULE_FORMAT } from './module_format.js'

const esmModuleValues = new Set(['es6', 'es2015', 'es2020', 'es2022', 'esnext', 'node16', 'nodenext'])

// Returns the module format that should be used for a TypeScript file at a
// given path, by reading the associated `tsconfig.json` file if it exists.
export const getModuleFormat = (path: string, boundary?: string) => {
const file = getTsconfig(path)

if (!file) {
return MODULE_FORMAT.COMMONJS
}

// If there is a boundary defined and the file we found is outside of it,
// discard the file.
if (boundary !== undefined && relative(boundary, dirname(file.path)).startsWith('..')) {
return MODULE_FORMAT.COMMONJS
}

const moduleProp = file.config.compilerOptions?.module?.toLowerCase() ?? ''

return esmModuleValues.has(moduleProp) ? MODULE_FORMAT.ESM : MODULE_FORMAT.COMMONJS
}

1 comment on commit 4bc75ba

@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: 3.2s
  • largeDepsNft: 10.9s
  • largeDepsZisi: 19.6s

Please sign in to comment.