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

fix: detect native bindings and change extension priority #353

Merged
merged 7 commits into from
Mar 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/bundler.js → src/node_bundler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ const { promisify } = require('util')
const esbuild = require('esbuild')
const semver = require('semver')

const { getPlugins } = require('./plugins')

const pUnlink = promisify(fs.unlink)

const bundleJsFile = async function ({
additionalModulePaths,
basePath,
destFilename,
destFolder,
externalModules = [],
Expand All @@ -20,13 +23,19 @@ const bundleJsFile = async function ({
const external = [...new Set([...externalModules, ...ignoredModules])]
const jsFilename = `${basename(destFilename, extname(destFilename))}.js`
const bundlePath = join(destFolder, jsFilename)
const pluginContext = {
nodeBindings: new Set(),
}

// esbuild's async build API throws on Node 8.x, so we switch to the sync
// version for that version range.
const shouldUseAsyncAPI = semver.satisfies(process.version, '>=9.x')
const supportsAsyncAPI = semver.satisfies(process.version, '>=9.x')

// The sync API does not support plugins.
const plugins = supportsAsyncAPI ? getPlugins({ additionalModulePaths, basePath, context: pluginContext }) : undefined

// eslint-disable-next-line node/no-sync
const buildFunction = shouldUseAsyncAPI ? esbuild.build : esbuild.buildSync
const buildFunction = supportsAsyncAPI ? esbuild.build : esbuild.buildSync
const data = await buildFunction({
bundle: true,
entryPoints: [srcFile],
Expand All @@ -35,6 +44,8 @@ const bundleJsFile = async function ({
outfile: bundlePath,
nodePaths: additionalModulePaths,
platform: 'node',
plugins,
resolveExtensions: ['.js', '.jsx', '.mjs', '.cjs', '.json'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should .es6 be added? This is an old extension that was used in the past when ES6 became a thing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! I'll make a note to do some testing and add that in a subsequent PR.

target: ['es2017'],
})
const cleanTempFiles = async () => {
Expand All @@ -44,8 +55,9 @@ const bundleJsFile = async function ({
// no-op
}
}
const additionalSrcFiles = [...pluginContext.nodeBindings]

return { bundlePath, cleanTempFiles, data }
return { bundlePath, cleanTempFiles, data: { ...data, additionalSrcFiles } }
}

module.exports = { bundleJsFile }
22 changes: 22 additions & 0 deletions src/node_bundler/plugins.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const { relative, resolve } = require('path')

const getNodeBindingHandlerPlugin = ({ basePath, context }) => ({
name: 'node-binding-handler',
setup(build) {
build.onResolve({ filter: /\.node$/ }, (args) => {
const fullPath = resolve(args.resolveDir, args.path)
const resolvedPath = relative(basePath, fullPath)

context.nodeBindings.add(fullPath)

return {
external: true,
path: resolvedPath,
}
})
},
})

const getPlugins = ({ basePath, context }) => [getNodeBindingHandlerPlugin({ basePath, context })]

module.exports = { getPlugins }
35 changes: 15 additions & 20 deletions src/runtimes/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { basename, dirname, join, normalize } = require('path')

const commonPathPrefix = require('common-path-prefix')

const { bundleJsFile } = require('../bundler')
const { bundleJsFile } = require('../node_bundler')
const {
getDependencyNamesAndPathsForDependencies,
getExternalAndIgnoredModulesFromSpecialCases,
Expand Down Expand Up @@ -51,6 +51,7 @@ const getSrcFilesAndExternalModules = async function ({
}
}

// eslint-disable-next-line max-statements
const zipFunction = async function ({
destFolder,
extension,
Expand All @@ -69,28 +70,21 @@ const zipFunction = async function ({
externalModules: externalModulesFromSpecialCases,
ignoredModules: ignoredModulesFromSpecialCases,
} = await getExternalAndIgnoredModulesFromSpecialCases({ srcDir })

// When a module is added to `externalModules`, we will traverse its main
// file recursively and look for all its dependencies, so that we can ship
// their files separately, inside a `node_modules` directory. Whenever we
// process a module this way, we can also flag it as external with esbuild
// since its source is already part of the artifact and there's no point in
// inlining it again in the bundle.
// As such, the dependency traversal logic will compile the names of these
// modules in `additionalExternalModules`.
const { moduleNames: externalModulesFromTraversal = [], paths: srcFiles } = await getSrcFilesAndExternalModules({
const externalModules = [...new Set([...externalModulesFromConfig, ...externalModulesFromSpecialCases])]
const { paths: srcFiles } = await getSrcFilesAndExternalModules({
stat,
mainFile,
extension,
srcPath,
srcDir,
pluginsModulesPath,
jsBundler,
jsExternalModules: [...new Set([...externalModulesFromConfig, ...externalModulesFromSpecialCases])],
jsExternalModules: externalModules,
})
const dirnames = srcFiles.map((filePath) => normalize(dirname(filePath)))

if (jsBundler === JS_BUNDLER_ZISI) {
const dirnames = srcFiles.map((filePath) => normalize(dirname(filePath)))

await zipNodeJs({
basePath: commonPathPrefix(dirnames),
destFolder,
Expand All @@ -104,16 +98,15 @@ const zipFunction = async function ({
return { bundler: JS_BUNDLER_ZISI, path: destPath }
}

const mainFilePath = dirname(mainFile)
const { bundlePath, data, cleanTempFiles } = await bundleJsFile({
additionalModulePaths: pluginsModulesPath ? [pluginsModulesPath] : [],
basePath: mainFilePath,
destFilename: filename,
destFolder,
externalModules: [
...externalModulesFromConfig,
...externalModulesFromSpecialCases,
...externalModulesFromTraversal,
],
externalModules,
ignoredModules: [...ignoredModulesFromConfig, ...ignoredModulesFromSpecialCases],
srcDir,
srcFile: mainFile,
})
const bundlerWarnings = data.warnings.length === 0 ? undefined : data.warnings
Expand All @@ -123,7 +116,9 @@ const zipFunction = async function ({
const aliases = {
[mainFile]: bundlePath,
}
const basePath = commonPathPrefix([...dirnames, dirname(mainFile)])
const srcFilesAfterBundling = [...srcFiles, ...data.additionalSrcFiles]
const dirnames = srcFilesAfterBundling.map((filePath) => normalize(dirname(filePath)))
const basePath = commonPathPrefix([...dirnames, mainFilePath])

try {
await zipNodeJs({
Expand All @@ -134,7 +129,7 @@ const zipFunction = async function ({
filename,
mainFile,
pluginsModulesPath,
srcFiles,
srcFiles: srcFilesAfterBundling,
})
} finally {
await cleanTempFiles()
Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/node-fetch/function.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const fetch = require('node-fetch')

module.exports = fetch

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

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

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

7 changes: 7 additions & 0 deletions tests/fixtures/node-fetch/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "function-requiring-node-fetch",
"version": "1.0.0",
"dependencies": {
"node-fetch": "^2.6.1"
}
}
47 changes: 39 additions & 8 deletions tests/main.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
const { readFile, chmod, symlink, unlink, rename } = require('fs')
const { tmpdir } = require('os')
const { normalize, resolve } = require('path')
const { platform } = require('process')
const { platform, version } = require('process')
const { promisify } = require('util')

const test = require('ava')
const cpy = require('cpy')
const del = require('del')
const execa = require('execa')
const pathExists = require('path-exists')
const semver = require('semver')
const { dir: getTmpDir, tmpName } = require('tmp-promise')
const unixify = require('unixify')

const { zipFunction, listFunctions, listFunctionsFiles } = require('..')
const { JS_BUNDLER_ESBUILD: ESBUILD, JS_BUNDLER_ESBUILD_ZISI: ESBUILD_ZISI } = require('../src/utils/consts')
Expand All @@ -24,6 +26,8 @@ const pSymlink = promisify(symlink)
const pUnlink = promisify(unlink)
const pRename = promisify(rename)

const supportsEsbuildPlugins = semver.satisfies(version, '>=9.x')

// Alias for the default bundler.
const DEFAULT = undefined
const EXECUTABLE_PERMISSION = 0o755
Expand Down Expand Up @@ -54,13 +58,29 @@ testBundlers('Zips Node.js function files', [ESBUILD, ESBUILD_ZISI, DEFAULT], as
t.true(files.every(({ runtime }) => runtime === 'js'))
})

testBundlers('Handles Node module with native bindings', [ESBUILD, ESBUILD_ZISI, DEFAULT], async (bundler, t) => {
const jsExternalModules = bundler === ESBUILD || bundler === ESBUILD ? ['test'] : undefined
const { files } = await zipNode(t, 'node-module-native', {
opts: { jsBundler: bundler, jsExternalModules },
})
t.true(files.every(({ runtime }) => runtime === 'js'))
})
// Bundling modules with native bindings with esbuild is expected to fail on
// Node 8 because esbuild plugins are not supported. The ZISI fallback should
// kick in.
testBundlers(
'Handles Node module with native bindings',
[ESBUILD_ZISI, DEFAULT, supportsEsbuildPlugins && ESBUILD].filter(Boolean),
async (bundler, t) => {
const { files, tmpDir } = await zipNode(t, 'node-module-native', {
opts: { jsBundler: bundler },
})
const requires = await getRequires({ filePath: resolve(tmpDir, 'src/function.js') })
const normalizedRequires = new Set(requires.map(unixify))

t.true(files.every(({ runtime }) => runtime === 'js'))
t.true(await pathExists(`${tmpDir}/src/node_modules/test/native.node`))

if (files[0].bundler === 'esbuild') {
t.true(normalizedRequires.has('node_modules/test/native.node'))
} else {
t.true(normalizedRequires.has('test'))
}
},
)

testBundlers('Can require node modules', [ESBUILD, ESBUILD_ZISI, DEFAULT], async (bundler, t) => {
await zipNode(t, 'local-node-module', { opts: { jsBundler: bundler } })
Expand Down Expand Up @@ -557,6 +577,17 @@ testBundlers(
},
)

testBundlers(
'Exposes the main export of `node-fetch` when imported using `require()`',
[ESBUILD, ESBUILD_ZISI, DEFAULT],
async (bundler, t) => {
const { files, tmpDir } = await zipFixture(t, 'node-fetch', { opts: { jsBundler: bundler } })
await unzipFiles(files)
// eslint-disable-next-line import/no-dynamic-require, node/global-require
t.true(typeof require(`${tmpDir}/function.js`) === 'function')
},
)

test('Zips Rust function files', async (t) => {
const { files, tmpDir } = await zipFixture(t, 'rust-simple', { length: 1 })

Expand Down