Dynamic-Imports via babel-register resulting in surplus "default module" - javascript

I have (polymorphic) code that is bundled using webpack including dynamic-imports (for code-splitting) and want to run the same code in NodeJS, currently using babel-register to be able to run the ES6 code.
I've encountered an issue when it comes to the dynamic imports: The resolved/loaded module seems to be wrapped in an additional (default-)module.
The following is a minimal example, using NodeJS v10.14.1, #babel/core#7.2.2, #babel/plugin-syntax-dynamic-import#7.2.0, #babel/preset-env#7.3.1, #babel/register#7.0.0:
index.js:
require('#babel/register')({
presets: ['#babel/preset-env'],
plugins: [
'#babel/plugin-syntax-dynamic-import'
]
})
require('./main').run()
dep.js:
export const bar = 3;
main.js:
import * as syncMod from './dep'
export function run() {
console.log(syncMod)
import('./dep').then(mod => console.log(mod))
}
Running this via node --experimental-modules index.js yields:
{ bar: 3 } // the syncMod
[Module] { default: { bar: 3 } } // the dynamically loaded
The "normal" import works as expected and directly gives the object with the exports. The dynamic-import does this too (as I would expect) in the bundled browser-version, but returns this [Module] { default: .. } thing around it in NodeJS.
Running without the babel-register code and rather using babel-node (#babel/node#7.2.2) (npx babel-node --experimental-modules --plugins=#babel/plugin-syntax-dynamic-import --presets=#babel/preset-env index) yields the same result.
I need to run it with the experimental flag, otherwise the dynamic import did not work at all (Not supported Error thrown).
I could access the dynamically-loaded module without further problems by accessing it below the added layer like mod.default.bar, but then my code would differ for browser and NodeJS.
I'm grateful for any explanation for this behaviour and maybe a solution (/where I went wrong) to get the normal/expected exports object.
I'm currently using this helper as a workaround:
function lazyImport(promise) {
if (typeof window !== 'undefined') {
// Browser
return promise
}
// Node
return promise.then(mod => mod.default)
}
and wrap every import() like this:
lazyImport(import('./path/to/mod')).then(mod => /* use the mod */)

Related

How can I use switch between different environments using the new cypress.config.js in Cypress 10x?

I used to have json files under my config folder containing variables for different environments. For example:
local.env.json would contain:
{
"baseUrl": "localhost:8080"
}
then another one called uat.env.json would contain:
{
"baseUrl": "https://uat.test.com"
}
and its configured on my plugins/index.ts as:
const version = config.env.version || 'uat'; // if version is not defined, default to this stable environment
config.env = require(`../../config/${version}.env.json`); // load env from json
I will then call it on my tests with cy.visit(Cypress.env().baseUrl)) then pass it on the CI with CYPRESS_VERSION=uat npx cypress run
However, with the new Cypress 10x version, the plugin file has been deprecated and just relies on cypress.config.js. I can't find any example on their documentation on how this can be done (I remember they used to have a page with these scenarios but can't find it now).
It's possible to use the old plugins/index.ts in the new cypress.config.ts by importing it.
This is the simplest example (with no new config in cypress.config.ts)
import { defineConfig } from 'cypress'
import legacyConfig from './cypress/plugins/index.js'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:1234',
setupNodeEvents(on, config) {
return legacyConfig(on, config) // call legacy config fn and return result
}
}
})
The typescript and modules may cause you grief about typings etc. I've not tried it in a typescript project, but it does work in a javascript project.
Alternatively, copy/paste everything from plugins/index.ts instead of calling the legacy function. This might be better as you add more plugins in the future.

How to use the TypeScript Compiler API to type-check modules imported using `require()`?

I am using TypeChecker from the TypeScript Compiler API in order to extract (inferred) type information for every node in the AST of my program. In particular, I try to find out the return values from imported module functions such as:
var vec3 = require('gl-matrix/vec3')
var myVec = vec3.fromValues(1, 2, 3) // $ExpectedType vec3
This works well for modules that were imported using the import { … } from '…' statement, but unfortunately, modules that were imported using require() like above are not recognized correctly, I only receive the type any for them. However, I have set both compiler options allowJs and checkJs.
Why are the types form require()d modules not inferred correctly? VS Code (which AFAIK relies on the same API?) is able to infer the types from require() statements as well, so I'd guess that in general, tsc is able of handling them. Are there any other compiler options that I need to set differently? Or is this indeed not supported and I need to use some other package for this?
Here is a minimum script to reproduce, I have also put it on repl.it together with two example files: https://replit.com/#LinqLover/typecheck-js
var ts = require("typescript")
// Run `node index.js sample-import.js`` to see the working TypeScript analysis
const files = process.argv[1] != "/run_dir/interp.js" ? process.argv.slice(2) : ["sample-require.js"]
console.log(`Analyzing ${files}:`)
const program = ts.createProgram(files, {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS,
allowJs: true,
checkJs: true
})
const checker = program.getTypeChecker()
for (const sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile) {
ts.forEachChild(sourceFile, visit)
}
}
function visit(node) {
try {
const type = checker.getTypeAtLocation(node)
console.log(checker.typeToString(type))
} catch (e) {
// EAFP
}
ts.forEachChild(node, visit)
}
Thank you so much in advance!
For follow-up, this turned out to be an issue with the type definitions for gl-matrix. I should better have tried out multiple packages before suspecting that the TypeScript engine itself could be broken ...
gl-matrix issue: https://github.com/toji/gl-matrix/issues/429

How do I correctly configure mocha tests with ts_transformer_keys?

I can't seem to set the custom transformer for ts-transform-keys with my mocha tests.
I’m using mocha 6.1.4
ts-node 8.3.0 https://www.npmjs.com/package/ts-node
ts-trasnformer-keys 0.3.5 https://github.com/kimamula/ts-transformer-keys
ttypescript 1.5.7 https://github.com/cevek/ttypescript
The ts-node documentation says that you cannot set a custom transformer on the CLI, only programatically. So I'm trying to use ttypescript to get around that restriction.
I've tried the following...
Note: test.ts contains the following
import { keys } from 'ts-transformer-keys';
describe("xyz"), () => {
it("123", (done) => {
keys<CustomInterface>();
});
});
Attempt 1) - Set the ts-node with an environment variable
TS_NODE_COMPILER="ttypescript" mocha test/test.ts --require ts-node/register
Then I have the following in test/tsconfig.json
{
"compilerOptions": {
"plugins": [
{ "transform": "ts-transformer-keys/transformer" }
]
}
}
This results in Uncaught TypeError: ts_transformer_keys_1.keys is not a function which indicates that the custom transformer wasn't used at compile time.
Attempt 2) Following the typescript API example from ts-transformer-keys
I added a mocha.opts file with the following
--file test/transformer-config.js
and a transformer-config.js file with the following
const ts = require('typescript');
const keysTransformer = require('ts-transformer-keys/transformer').default;
const program = ts.createProgram(['test/test.ts'], {
strict: true,
noEmitOnError: true,
target: ts.ScriptTarget.ES5
});
const transformers = {
before: [keysTransformer(program)],
after: []
};
const { emitSkipped, diagnostics } = program.emit(undefined, undefined, undefined, false, transformers);
if (emitSkipped) {
throw new Error(diagnostics.map(diagnostic => diagnostic.messageText).join('\n'));
}
Then I invoke it like this mocha test/test.ts --require ts-node/register
This results in the following error
/Users/jjohnson/Documents/OCS/hmp/git/hmp-server/server/test/ttypescript-register.js:17
throw new Error(diagnostics.map(diagnostic => diagnostic.messageText).join('\n'));
^
Error: [object Object]
[object Object]
[object Object]
at Object.<anonymous> (/Users/jjohnson/Documents/OCS/hmp/git/hmp-server/server/test/ttypescript-register.js:17:9)
at Module._compile (internal/modules/cjs/loader.js:777:30)
...
It feels like in Attempt 1 it wasn't ever calling the code that sets the custom transformer in tsconfig.json or if it was getting called the code was failing silently.
It feels like in Attempt 2 I'm creating a new instance of the typescript program and then that fails for some reason. And even if it succeeded I'm not sure that this is the right way to go about configuring things since the ts.createProgram wants a list of RootNames for the files it will transpile.
Maybe my entire approach is wrong.
All I really want is a way that in my mocha tests I can verify that the expected result type is what the method returned. And I'd like to be able to do this w/out touching too much of the source code.
you should be able to define your required module (see below) and run ts-node programmatically. In this way, you can safely use any customer transformer.
// tsnode.js
const transformer = require('ts-transformer-keys/transformer').default;
require("ts-node").register({
transformers: program => ({
before: [
transformer(program)
]
})
});
then you can run mocha with require
mocha --require './tsnode.js' --watch-extensions ts,tsx "test/**/*.{ts,tsx}
You can tell ts-node which compiler to use in tsconfig.json. This is covered in the ts-node docs. If your using transformers presumably your also using ttypescript compiler. You just need to add this:
"ts-node": {
"compiler": "ttypescript"
}

Dynamic bundle loading at runtime

Consider a bundle in some project packed with webpack and all dependencies to bundle1.js:
// entry.ts
import * as _ from 'lodash';
// other imported libraries
import { something } from './util';
// other imported bundle project local code
export function doBundleSomething(args: any): any {
// actually does something necessary
console.log('doing things');
return 'whatever result';
}
Shell program is another project also packed with webpack and all dependencies to main.js, except bundle1.js, because it is decided at run time from environment:
// main.ts
import * as _ from 'lodash';
// other imported libraries
import { something } from './shell-util';
// other imported shell program local code
// do something useful
const ifBundleNeeded = true; // decided by useful something
if (ifBundleNeeded) {
const bundlePath = process.env.BUNDLE_PATH; // could be CLI argument
// somehow load bundle
const doBundleSomething = ???
const result = doBundleSomething({arg: 'hello-bundle'});
console.log('bundle said', result);
}
Should be executed like:
BUNDLE_PATH=/opt/path/some-bundle.js node /srv/program/path/main.js
Notes:
bare minimum possible configurations are used for everything
tsconfig targets for both bundles is es6
typescript >= 3.x with webpack awesome-typescript-loader (i.e. latest and greatest)
duplication of code in both bundles is unavoidable and acceptable
security issues are not of concern (like code injection etc.)
How to achieve this behavior with bare minimum configs?
If BUNDLE_PATH is known (i.e. hard-coding path, so that webpack sees it) at webpacking of shell program, it works in different variations with require, import() including eval("require")().
Otherwise, using different alternatives fail at different stages with various errors from Cannot find module to undefined when used.

Packaging-up Browser/Server CommonJS modules with dependancies

Lets say I'm writing a module in JavaScript which can be used on both the browser and the server (with Node). Lets call it Module. And lets say that that Module would benefit from methods in another module called Dependancy. Both of these modules have been written to be used by both the browser and the server, à la CommonJS style:
module.js
if (typeof module !== 'undefined' && module.exports)
module.exports = Module; /* server */
else
this.Module = Module; /* browser */
dependancy.js
if (typeof module !== 'undefined' && module.exports)
module.exports = Dependancy; /* server */
else
this.Dependancy = Dependancy; /* browser */
Obviously, Dependancy can be used straight-out-of-the-box in a browser. But if Module contains a var dependancy = require('dependency'); directive in it, it becomes more of a hassle to 'maintain' the module.
I know that I could perform a global check for Dependancy within Module, like this:
var dependancy = this.Dependancy || require('dependancy');
But that means my Module has two added requirements for browser installation:
the user must include the dependency.js file as a <script> in their document
and the user must make sure this script is loaded before module.js
Adding those two requirements throws the idea of an easy-going modular framework like CommonJS.
The other option for me is that I include a second, compiled script in my Module package with the dependency.js bundled using browserify. I then instruct users who are using the script in the browser to include this script, while server-side users use the un-bundled entry script outlined in the package.json. This is preferable to the first way, but it requires a pre-compilation process which I would have to run every time I changed the library (for example, before uploading to GitHub).
Is there any other way of doing this that I haven't thought of?
The two answers currently given are both very useful, and have helped me to arrive at my current solution. But, as per my comments, they don't quite satisfy my particular requirements of both portability vs ease-of-use (both for the client and the module maintainer).
What I found, in the end, was a particular flag in the browserify command line interface that can bundle the modules and expose them as global variables AND be used within RequireJS (if needed). Browserify (and others) call this Universal Module Definition (UMD). Some more about that here.
By passing the --standalone flag in a browserify command, I can set my module up for UMD easily.
So...
Here's the package.js for Module:
{
"name": "module",
"version": "0.0.1",
"description": "My module that requires another module (dependancy)",
"main": "index.js",
"scripts": {
"bundle": "browserify --standalone module index.js > module.js"
},
"author": "shennan",
"devDependencies": {
"dependancy": "*",
"browserify": "*"
}
}
So, when at the root of my module, I can run this in the command line:
$ npm run-script bundle
Which bundles up the dependancies into one file, and exposes them as per the UMD methodology. This means I can bootstrap the module in three different ways:
NodeJS
var Module = require('module');
/* use Module */
Browser Vanilla
<script src="module.js"></script>
<script>
var Module = module;
/* use Module */
</script>
Browser with RequireJS
<script src="require.js"></script>
<script>
requirejs(['module.js'], function (Module) {
/* use Module */
});
</script>
Thanks again for everybody's input. All of the answers are valid and I encourage everyone to try them all as different use-cases will require different solutions.
Of course you could use the same module with dependency on both sides. You just need to specify it better. This is the way I use:
(function (name, definition){
if (typeof define === 'function'){ // AMD
define(definition);
} else if (typeof module !== 'undefined' && module.exports) { // Node.js
module.exports = definition();
} else { // Browser
var theModule = definition(), global = this, old = global[name];
theModule.noConflict = function () {
global[name] = old;
return theModule;
};
global[name] = theModule;
}
})('Dependency', function () {
// return the module's API
return {
'key': 'value'
};
});
This is just a very basic sample - you can return function, instantiate function or do whatever you like. In my case I'm returning an object.
Now let's say this is the Dependency class. Your Module class should look pretty much the same, but it should have a dependency to Dependency like:
function (require, exports, module) {
var dependency = require('Dependency');
}
In RequireJS this is called Simplified CommonJS Wrapper: http://requirejs.org/docs/api.html#cjsmodule
Because there is a require statement at the beginning of your code, it will be matched as a dependency and therefore it will either be lazy loaded or if you optimize it - marked as a dependency early on (it will convert define(definition) to define(['Dependency'], definition) automatically).
The only problem here is to keep the same path to the files. Keep in mind that nested requires (if-else) won't work in Require (read the docs), so I had to do something like:
var dependency;
try {
dependency = require('./Dependency'); // node module in the same folder
} catch(err) { // it's not node
try {
dependency = require('Dependency'); // requirejs
} catch(err) { }
}
This worked perfectly for me. It's a bit tricky with all those paths, but at the end of the day, you get your two separate modules in different files which can be used on both ends without any kind of checks or hacks - they have all their dependencies are work like a charm :)
Take a look at webpack bundler.
You can write module and export it via module exports. Then You can in server use that where you have module.export and for browser build with webpack. Configuration file usage would be the best option
module.exports = {
entry: "./myModule",
output: {
path: "dist",
filename: "myModule.js",
library: "myModule",
libraryTarget: "var"
}
};
This will take myModule and export it to myModule.js file. Inside module will be assigned to var (libraryTarget flag) named myModule (library flag).
It can be exported as commonJS module, war, this, function
Since bundling is node script, this flag values can be grammatically set.
Take a look at externals flag. It is used if you want to have special behavior for some dependencies. For example you are creating react component and in your module you want to require it but not when you are bundling for web because it already is there.
Hope it is what you are looking for.

Categories

Resources