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

Commit

Permalink
feat: add support for TypeScript to NFT (#1515)
Browse files Browse the repository at this point in the history
* feat: add support for TypeScript to NFT

* feat: add rewrites

* refactor: stop using esbuild for v2 functions
  • Loading branch information
eduardoboucas committed Aug 1, 2023
1 parent f345c7c commit 9d3cc9a
Show file tree
Hide file tree
Showing 9 changed files with 79 additions and 31 deletions.
4 changes: 0 additions & 4 deletions src/runtimes/node/bundlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,6 @@ const getDefaultBundler = async ({
runtimeAPIVersion: number
}): Promise<NodeBundlerName> => {
if (runtimeAPIVersion === 2) {
if (ESBUILD_EXTENSIONS.has(extension)) {
return NODE_BUNDLER.ESBUILD
}

return NODE_BUNDLER.NFT
}

Expand Down
7 changes: 6 additions & 1 deletion src/runtimes/node/bundlers/nft/es_modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,12 @@ const transpileESM = async ({
await Promise.all(
pathsToTranspile.map(async (path) => {
const absolutePath = resolvePath(path, basePath)
const transpiled = await transpile(absolutePath, config, name)
const transpiled = await transpile({
config,
format: MODULE_FORMAT.COMMONJS,
name,
path: absolutePath,
})

rewrites.set(absolutePath, transpiled)
}),
Expand Down
40 changes: 37 additions & 3 deletions src/runtimes/node/bundlers/nft/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { basename, dirname, join, normalize, resolve } from 'path'
import { basename, dirname, join, normalize, resolve, extname } from 'path'

import { nodeFileTrace } from '@vercel/nft'
import resolveDependency from '@vercel/nft/out/resolve-dependency.js'

import type { FunctionConfig } from '../../../../config.js'
import { FeatureFlags } from '../../../../feature_flags.js'
import type { RuntimeCache } from '../../../../utils/cache.js'
import { cachedReadFile } from '../../../../utils/fs.js'
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 { getNodeSupportMatrix } from '../../utils/node_version.js'
import type { GetSrcFilesFunction, BundleFunction } from '../types.js'

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

const appearsToBeModuleName = (name: string) => !name.startsWith('.')

Expand All @@ -34,6 +36,8 @@ const bundle: BundleFunction = async ({
includedFilesBasePath || basePath,
)
const {
aliases,
mainFile: normalizedMainFile,
moduleFormat,
paths: dependencyPaths,
rewrites,
Expand All @@ -55,10 +59,11 @@ const bundle: BundleFunction = async ({
const srcFiles = [...filteredIncludedPaths].sort()

return {
aliases,
basePath: getBasePath(dirnames),
includedFiles: includedPaths,
inputs: dependencyPaths,
mainFile,
mainFile: normalizedMainFile,
moduleFormat,
rewrites,
srcFiles,
Expand Down Expand Up @@ -97,6 +102,10 @@ const traceFilesAndTranspile = async function ({
name: string
runtimeAPIVersion: number
}) {
const isTypeScript = extname(mainFile) === '.ts'
const tsAliases = new Map<string, string>()
const tsRewrites = new Map<string, string>()

const {
fileList: dependencyPaths,
esmFileList,
Expand All @@ -109,6 +118,19 @@ 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')

// Overriding the contents of the `.ts` file.
tsRewrites.set(path, transpiledSource)

// Rewriting the `.ts` path to `.js` in the bundle.
tsAliases.set(path, newPath)

return transpiledSource
}

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

return source
Expand Down Expand Up @@ -140,6 +162,17 @@ const traceFilesAndTranspile = async function ({
const normalizedDependencyPaths = [...dependencyPaths].map((path) =>
basePath ? resolve(basePath, path) : resolve(path),
)

if (isTypeScript) {
return {
aliases: tsAliases,
mainFile: getPathWithExtension(mainFile, '.js'),
moduleFormat: MODULE_FORMAT.ESM,
paths: normalizedDependencyPaths,
rewrites: tsRewrites,
}
}

const { moduleFormat, rewrites } = await processESM({
basePath,
cache,
Expand All @@ -153,6 +186,7 @@ const traceFilesAndTranspile = async function ({
})

return {
mainFile,
moduleFormat,
paths: normalizedDependencyPaths,
rewrites,
Expand Down
15 changes: 11 additions & 4 deletions src/runtimes/node/bundlers/nft/transpile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ import { build } from '@netlify/esbuild'
import type { FunctionConfig } from '../../../../config.js'
import { FunctionBundlingUserError } from '../../../../utils/error.js'
import { RUNTIME } from '../../../runtime.js'
import { MODULE_FORMAT } from '../../utils/module_format.js'
import { ModuleFormat } from '../../utils/module_format.js'
import { getBundlerTarget } from '../esbuild/bundler_target.js'
import { NODE_BUNDLER } from '../types.js'

export const transpile = async (path: string, config: FunctionConfig, functionName: string) => {
interface TranspileOptions {
config: FunctionConfig
format?: ModuleFormat
name: string
path: string
}

export const transpile = async ({ config, format, name, path }: TranspileOptions) => {
// 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)
Expand All @@ -16,7 +23,7 @@ export const transpile = async (path: string, config: FunctionConfig, functionNa
const transpiled = await build({
bundle: false,
entryPoints: [path],
format: MODULE_FORMAT.COMMONJS,
format,
logLevel: 'error',
platform: 'node',
sourcemap: Boolean(config.nodeSourcemap),
Expand All @@ -27,7 +34,7 @@ export const transpile = async (path: string, config: FunctionConfig, functionNa
return transpiled.outputFiles[0].text
} catch (error) {
throw FunctionBundlingUserError.addCustomErrorInfo(error, {
functionName,
functionName: name,
runtime: RUNTIME.JAVASCRIPT,
bundler: NODE_BUNDLER.NFT,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
type MyType = string

export const type: MyType = '❤️ TypeScript'
const type: MyType = '❤️ TypeScript'

export const handler = () => type
4 changes: 3 additions & 1 deletion tests/fixtures/node-typescript-directory-2/function/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
type MyType = string

export const type: MyType = '❤️ TypeScript'
const type: MyType = '❤️ TypeScript'

export const handler = () => type
7 changes: 3 additions & 4 deletions tests/fixtures/node-typescript-with-imports/function.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// We do not rename to `./util.js` because `@vercel/nft` does not manage to
// find the dependency `util.ts` then, even though using `.js` file extensions
// is the recommended way to use pure ES modules with Typescript.
export { type } from './lib/util'
import { type } from './lib/util.js'

export const handler = () => type
4 changes: 3 additions & 1 deletion tests/fixtures/node-typescript/function.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
type MyType = string

export const type: MyType = '❤️ TypeScript'
const type: MyType = '❤️ TypeScript'

export const handler = () => type
25 changes: 13 additions & 12 deletions tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -929,53 +929,54 @@ describe('zip-it-and-ship-it', () => {

testMany(
'Handles a TypeScript function ({name}.ts)',
['bundler_default', 'bundler_esbuild', 'bundler_esbuild_zisi', 'bundler_default_nft', 'todo:bundler_nft'],
['bundler_default', 'bundler_esbuild', 'bundler_esbuild_zisi', 'bundler_default_nft', 'bundler_nft'],
async (options) => {
const { files } = await zipFixture('node-typescript', {
opts: options,
})
const unzippedFunctions = await unzipFiles(files)
const { type } = await importFunctionFile(`${unzippedFunctions[0].unzipPath}/function.js`)
expect(type).toBeTypeOf('string')
const { handler } = await importFunctionFile(`${unzippedFunctions[0].unzipPath}/function.js`)
expect(handler()).toBe('❤️ TypeScript')
},
)

testMany(
'Handles a TypeScript function ({name}/{name}.ts)',
['bundler_default', 'bundler_esbuild', 'bundler_esbuild_zisi', 'bundler_default_nft', 'todo:bundler_nft'],
['bundler_default', 'bundler_esbuild', 'bundler_esbuild_zisi', 'bundler_default_nft', 'bundler_nft'],
async (options) => {
const { files } = await zipFixture('node-typescript-directory-1', {
opts: options,
})
const unzippedFunctions = await unzipFiles(files)
const { type } = await importFunctionFile(`${unzippedFunctions[0].unzipPath}/function.js`)
expect(type).toBeTypeOf('string')
const { handler } = await importFunctionFile(`${unzippedFunctions[0].unzipPath}/function.js`)
expect(handler()).toBe('❤️ TypeScript')
},
)

testMany(
'Handles a TypeScript function ({name}/index.ts)',
['bundler_default', 'bundler_esbuild', 'bundler_esbuild_zisi', 'bundler_default_nft', 'todo:bundler_nft'],
['bundler_default', 'bundler_esbuild', 'bundler_esbuild_zisi', 'bundler_default_nft', 'bundler_nft'],
async (options) => {
const { files } = await zipFixture('node-typescript-directory-2', {
opts: options,
})
const unzippedFunctions = await unzipFiles(files)
const { type } = await importFunctionFile(`${unzippedFunctions[0].unzipPath}/function.js`)
expect(type).toBeTypeOf('string')
const { handler } = await importFunctionFile(`${unzippedFunctions[0].unzipPath}/function.js`)
expect(handler()).toBe('❤️ TypeScript')
},
)

testMany(
'Handles a TypeScript function with imports',
['bundler_default', 'bundler_esbuild', 'bundler_esbuild_zisi', 'bundler_default_nft', 'todo:bundler_nft'],
['bundler_default', 'bundler_esbuild', 'bundler_esbuild_zisi', 'bundler_default_nft', 'bundler_nft'],
async (options) => {
const { files } = await zipFixture('node-typescript-with-imports', {
opts: options,
})
const unzippedFunctions = await unzipFiles(files)
const { type } = await importFunctionFile(`${unzippedFunctions[0].unzipPath}/function.js`)
expect(type).toBeTypeOf('string')
const { handler } = await importFunctionFile(`${unzippedFunctions[0].unzipPath}/function.js`)

expect(handler()).toBe('❤️ TypeScript')
},
)

Expand Down

1 comment on commit 9d3cc9a

@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.6s
  • largeDepsNft: 11.5s
  • largeDepsZisi: 21.1s

Please sign in to comment.