typescript-bundle icon indicating copy to clipboard operation
typescript-bundle copied to clipboard

require.main is undefined

Open jacobdweightman opened this issue 6 years ago • 3 comments
trafficstars

In Node, a common idiom for checking if the current module is the one that was directly invoked is if (require.main === module) { ... }. Here are the docs.

This works as expected when the module is "inlined" in the top-level IIFE of the generated JavaScript code (see my other issue), but when this code is used inside of a defined module this condition fails because require is not the expected object.

This seems to be caused by require being provided by dependency injection, because the injected require object is a function rather than its usual object value.

jacobdweightman avatar Jul 13 '19 23:07 jacobdweightman

This looks quite interesting. TypeScript-Bundle avoids testing for various JS environment conditions (such as require.main) but does provide a proxy for CommonJS require as a fallback for when the bundles resolver fails to resolve at runtime (noting that TypeScript-Bundle uses AMD as its module target). The proxy itself is just a function without the main property on it.

The proxy for require is injected as a dependency into the define itself, with TypeScript's AMD output emitted as....

// note that tsc-bundle will proxy to nodes cjs require for un-resolvable AMD modules.
define("main", ["require", "exports", ...], (require, exports, ...) => {
    
    const fs = require('fs') // this is possible
})

You can test with the following.

$ tsc-bundle ./main.ts --outFile bundle.js --watch

Which should work in both emit forms (AMD and non-AMD depending on the if the module has a import / export).

const fs = require('fs')

console.log(fs.writeFile)

export const foo = 1 // comment this line

Do you have any suggestions for improving upon this? Curious about the various node idioms around modules that might lead to better patterns for resolving CJS modules through AMD. Open to your thoughts.

sinclairzx81 avatar Jul 14 '19 00:07 sinclairzx81

What is this proxy to require, exactly? Why is it a proxy? Would it be possible to directly inject the require object, if it is provided by the environment?

My thinking here is that a bundler should ensure that the behavior of

tsc -p ./tsconfig.json
node path/to/entrypoint.js

should be identical to the behavior of

tsc-bundle ./tsconfig.json
node path/to/bundle.js

jacobdweightman avatar Jul 14 '19 01:07 jacobdweightman

Yes, both tsc -p ./tsconfig.json and tsc-bundle ./tsconfig.json should work the same with respect to paths.

In terms of the require proxy, it has more to do with resolving CJS modules in node within the bundle. let me explain.

The require() function that gets injected as a AMD dependency resolves both on behalf of the bundle (so resolving modules internal to the bundle) as well as to the nodes cjs require if available. The require() passed in just proxies the request to nodes CJS require function IF it can't resolve from the bundle itself. The logic for it looks a bit like this.

// Function passed as AMD 'require' dependency.
function proxy_require(module_name: string) {

    try { return amd_bundle_resolve(module_name)  } catch { /* ignore */ }

    try { return require(module_name)  } catch { /* ignore */ }
    
    // ... other resolution strategies here

    throw Error(`unable to resolve module ${module_name}`)
}

So, the name property is unavailable for the require() that gets passed in. So it won't be possible to check for if(require.main === module) { ... } at runtime if running a bundle in node. However it supports the following usage patterns if you need to resolve with or without CJS require()

import syntax

The following would be the typical TypeScript approach to import modules using import. Here 'fs' will be resolved from the node cjs require, and foo from the internal bundle.

import * as foo from './foo'
import * as fs from 'fs'

define('foo', ['require', 'exports'], (require, exports) {
     exports.bar = 1
})
define('main', ['require', 'exports', 'foo', 'fs'], (require, exports, foo, fs) {
   console.log(foo)
   console.log(fs)
})

late via 'require' dependency.

A module can also be fetched using the require dependency passed into the AMD definition. This is less common usage, but was written to handle conditional and late require for node (which is a pattern you see sometimes in node applications). It provides a way to resolve modules in a CJS fashion, but its not something I would recommend using.

define('foo', ['require', 'exports'], (require, exports) {
     exports.bar = 1
})
define('main', ['require', 'exports'], (require, exports) {
   const foo = require('foo')
   const fs = require('fs')
   console.log(foo)
   console.log(fs)
})

sinclairzx81 avatar Jul 14 '19 02:07 sinclairzx81