Strange behaviour using legacy packages with ESM modules - node.js

I am trying to use the calculate-size package in my app that uses ES modules. This package is very old and declares it's exports using the following code:
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = function() { /* ... */ }
However when I try to import the package as follows I experience some strange behaviour:
// index.mjs
import fn from "calculate-size";
console.log(fn)
I expect the output of this to be that fn is a function however it is an object containing a property named default. It would be possible for me to use fn.default however this would conflict with the package's typings.
I would like to understand some more information about this behaviour as I haven't been able to find any documentation about this issue. I also want to understand if there is a fix on my end of if the only solution is to contact the package maintainer.

Related

Create a TypeScript library with optional dependencies resolved by application

I've written a library published to a private npm repo which is used by my applications.
This library contains utilities and has dependencies to other libraries, as an example let's choose #aws-sdk/client-lambda.
Some of my applications use only some of the utilities and don't need the dependencies to the external libraries, while some applications use all of the utilities.
To avoid having all applications getting a lot of indirect dependencies they don't need, I tried declaring the dependencies as peerDependencies and having the applications resolve the ones they need. It works well to publish the package, and to use it from applications who declare all of the peerDependencies as their own local dependencies, but applications failing to declare one of the dependencies get build errors when the included .d.ts files of the library are imported in application code:
error TS2307: Cannot find module '#aws-sdk/client-kms' or its corresponding type declarations.
Is it possible to resolve this situation so that my library can contain many different utils but the applications may "cherry-pick" the dependencies they need to fulfill the requirements of those utilities in runtime?
Do I have to use dynamic imports to do this or is there another way?
I tried using #ts-ignore in the library code, and it was propagated to the d.ts file imported by the applications, but it did not help.
Setup:
my-library
package.json:
peerDependencies: {
"#aws-sdk/client-lambda": "^3.27.0"
}
foo.ts:
import {Lambda} from '#aws-sdk/client-lambda';
export function foo(lambda: Lambda): void {
...
}
bar.ts:
export function bar(): void {
...
}
index.ts:
export * from './foo';
export * from './bar';
my-application1 - works fine
package.json:
dependencies: {
"my-library": "1.0.0",
"#aws-sdk/client-lambda": "^3.27.0"
}
test.ts:
import {foo} from 'my-library';
foo();
my-application2 - does not compile
package.json:
dependencies: {
"my-library": ...
}
test:ts:
import {bar} from 'my-library';
bar();
I found two ways of dealing with this:
1. Only use dynamic imports for the optional dependencies
If you make sure that types exported by the root file of the package only include types and interfaces and not classes etc, the transpiled JS will not contain any require statement to the optional library. Then use dynamic imports to import the optional library from a function so that they are required only when the client explicitly uses those parts of the library.
In the case of #aws-sdk/client-lambda, which was one of my optional dependencies, I wanted to expose function that could take an instance of a Lambda object or create one itself:
import {Lambda} from '#aws-sdk/client-lambda';
export function foo(options: {lambda?: Lambda}) {
if (!lambda) {
lambda = new Lambda({ ... });
}
...
}
Since Lambda is a class, it will be part of the transpiled JS as a require statement, so this does not work as an optional dependency. So I had to 1) make that import dynamic and 2) define an interface to be used in place of Lambda in my function's arguments to get rid of the require statement on the package's root path. Unfortunately in this particular case, the AWS SDK does not offer any type or interface which the class implements, so I had to come up with a minimal type such as
export interface AwsClient {
config: {
apiVersion: string;
}
}
... but of course, lacking a type ot represent the Lambda class, you might even resort to any.
Then comes the dynamic import part:
export async function foo(options: {lambda?: AwsClient}) {
if (!lambda) {
const {Lambda} = await import('#aws-sdk/client-lambda');
lambda = new Lambda({ ... });
}
...
}
With this code, there is no longer any require('#aws-sdk/client-lambda') on the root path of the package, only within the foo function. Only clients calling the foo function will have to have the dependency in their
node_modules.
As you can see, a side-effect of this is that every function using the optional library must be async since dynamic imports return promises. In my case this worked out, but it may complicate things. In one case I had a non-async function (such as a class constructor) needing an optional library, so I had no choice but to cache the promised import and resolve it later when used from an async member function, or do a lazy import when needed. This has the potential of cluttering code badly ...
So, to summarize:
Make sure any code that imports code from the optional library is put inside functions that the client wanting to use that functionality calls
It's OK to have imports of types from the optional library in the root of your package as it's stripped out when transpiled
If needed, defined substitute types to act as place-holders for any class arguments (as classes are both types and code!)
Transpile and investigate the resulting JS to see if you have any require statement for the optional library in the root, if so, you've missed something.
Note that if using webpack etc, using dynamic imports can be tricky as well. If the import paths are constants, it usually works, but building the path dynamically (await import('#aws-sdk/' + clientName)) will usually not unless you give webpack hints. This had me puzzled for a while since I wrote a wrapper in front of my optional AWS dependencies, which ended up not working at all for this reason.
2. Put the files using the optional dependencies in .ts files not exported by the root file of the package (i.e., index.ts).
This means that clients wanting to use the optional functionality must
import those files by sub-path, such as:
import {OptionalStuff} from 'my-library/dist/optional;
... which is obviously less than ideal.
in my case, the typescript IDE in vscode fails to import the optional type, so im using the relative import path
// fix: Cannot find module 'windows-process-tree' or its corresponding type declarations
//import type * as WindowsProcessTree from 'windows-process-tree';
import type * as WindowsProcessTree from '../../../../../node_modules/#types/windows-process-tree';
// global variable
let windowsProcessTree: typeof WindowsProcessTree;
if (true) { // some condition
windowsProcessTree = await import('windows-process-tree');
windowsProcessTree.getProcessTree(rootProcessId, tree => {
// ...
});
}
package.json
{
"devDependencies": {
"#types/windows-process-tree": "^0.2.0",
},
"optionalDependencies": {
"windows-process-tree": "^0.3.4"
}
}
based on vscode/src/vs/platform/terminal/node/windowsShellHelper.ts

How do I import a rust WASM module in gatsby js?

I'm trying to use my rust module from the rust webassembly book in my gatsby project. When I try to import the module like so:
import { <rust-struct> } from 'rust_wasm_npm_package';
I get the following error:
The module seem to be a WebAssembly module, but module is not flagged as WebAssembly module for
webpack.
BREAKING CHANGE: Since webpack 5 WebAssembly is not enabled by default and flagged as experimental
feature.
You need to enable one of the WebAssembly experiments via 'experiments.asyncWebAssembly: true' (based
on async modules) or 'experiments.syncWebAssembly: true' (like webpack 4, deprecated).
For files that transpile to WebAssembly, make sure to set the module type in the 'module.rules'
section of the config (e. g. 'type: "webassembly/async"').
(Source code omitted for this binary file)
I'm unable to add the experiments option to the gatsby config file, so I'm not sure what is the best way to import a wasm-pack rust module into gatsby.
I was able to get this working by adding a gatsby-node.js file with the following code:
exports.onCreateWebpackConfig = ({ actions }) => {
actions.setWebpackConfig({
experiments: {
syncWebAssembly: true,
},
});
};
I was then able to import the web assembly asynchronously. Not sure why I did not need to use asyncWebassembly: true instead, but it works!
// The reason for this useless concatenation
// is to get rid of a really specific issue
// with Webpack and WASM modules being imported
// all in one line.
/*eslint no-useless-concat: "off"*/
const module = await import("path/" + "toJSFile.js");
const memModule = await import("path/" + "toWasmModule.wasm");
const memory = memModule.memory;
setMem(memory);

Converting CommonJS to AMD

I have a simple CommonJS module published on NPM and Bower that basically just looks like this:
function Foo(){}
module.exports = new Foo();
now the easiest way to convert to AMD format would be just to publish a second version that was AMD compatible like so:
define(function (require, exports, module) {
function Foo(){}
module.exports = new Foo();
});
but I thought there was a way you can shim the CommonJS module using requirejs.config like so:
requirejs.config({
paths:{
'foo':'assets/vendor/foo'
},
shim:{
'foo' :{
exports: 'Foo'
}
}
});
but this doesn't seem to work. I figure that the shim tool does the wrapping that I did above for you, but I am not entirely sure.
The Problem
shim cannot do as much as you want it to do. When RequireJS evaluates the symbol you give in exports, it evaluates it in the global space. So using exports works fine if you can access the variable you want to export from the global space. If library foo exports Foo into the global space, that works fine.
What you are trying to do is have a module that exports its API through module work with a shim. This cannot work because RequireJS cannot guess that the module being loaded requires that a global module object be already available to the module being loaded. In the simplest case, loading such module will result in a ReferenceError because module does not exist. There would be ways to fake the module.exports thing that would work in the most trivial cases but it would not work as a general solution.
A Solution
You can use r.js to convert files that are written in the CommonJS format to what RequireJS needs. It is documented here. The documentation gives the following synopsis:
node r.js -convert path/to/commonjs/modules/ path/to/output
You should also read this part of the README for some caveats.

TypeScript 0.8.2 importing Node.js modules in internal modules

Okay, as I can see you would like to use internal modules in your project. Well, there was a workaround in TypeScript 0.8.1.1, you could define non exported module (internal) and add imports above it. In 0.8.2 it seems that this doesn't work anymore. Only option I see here would be to completely omit import syntax and use standard require for node modules. I don't know if this is a good idea but please, share your opinions. I know that using import syntax will make module external (language specification), but that wasn't true in 0.8.1.1, bug maybe?
In TypeScript 0.8.1.1 this worked and doesn't work in 0.8.2 anymore:
import path = module('path');
import fs = module('fs');
module SomeNamespace.Controller {
export class Index {
...
}
}
I could reference file including above code using reference syntax on top of file in other internal modules and normally call:
var ctrl = new SomeNamespace.Controller.Index;
ctrl.index();
It seems that in 0.8.2 this is the only way what it works for internal modules:
var path = require('path');
var fs = require('fs');
module SomeNamespace.Controller {
export class Index {
...
}
}
Are there any other possibilities to mix internal modules with Node.js modules? Is there something wrong with above require usage (it compiles and runs okay ...)?
I think that TypeScript 0.8.2 takes us closer to the specification.
The syntax:
import x = module('SomeModule');
Is specifically an ExternalModuleReference in the TypeScript Language Specification.
An internal module would be imported using:
///<reference path="SomeModule.ts" />
import x = SomeModule;
But importing an internal module won't generate you a require statement in your JavaScript.
Taken from TypeScript Language Specification 0.8 - 9.2.2 Import Declarations
ImportDeclaration:
import Identifier = ModuleReference ;
ModuleReference:
ExternalModuleReference
ModuleName
ExternalModuleReference:
module ( StringLiteral )
Ok, this error is due to the version of the TypeScript.
In TypeScript 0.8.1.1 to import an external module the syntax has to be:
export import <moduleName> = module(“<path>”);
This is a bug identified in the latest version of TypeScript, you can return to the previous version or change the syntax to make it compatible with v0.8.1.1. Have in mind that this is a bug and in future versions, you should be able to use the original syntax.
This is the official thread for this bug:
http://typescript.codeplex.com/discussions/405800

Invoke the text plugin from requirejs mapping

I'm writing a web app using TypeScript, Backbone, and Mustache. I want to use Requirejs for dependency loading.
I'm also using the Web Essentials visual studio plugin for TypeScript with the AMD compilation option turned on. For those that are not familiar with this, it will wrap your type script file in an AMD module if you import external modules.
For example:
In type script I import the following modules in type definition files.
export import Backbone = module("Backbone");
import mainTemplate = module("MainTemplate");
The output is something like:
define(["require", "exports", "Backbone", "MainTemplate"], function(require, exports, __Backbone__, __mainTemplate__) {
//...code goes here ...
});
For the template, I've declared the following in a type definition file:
declare module "MainTemplate" { }
In order to support requirejs plugins, you need to declare your module as:
declare module "text!MainTemplate.html" { }
I'd like to keep the module name free of plugins and file extensions. This would leave me with some flexibility in the future.
I have the following mapping in require.
require.config({
map: {
"MyModule": {
"MainTemplate": "text!MainTemplate.html"
}
}
}
This successfully invokes the text plugin however, the plugin loads the wrong url. Sifting through the source code for the text plugin, I found that the following code is the culprit.
load: function (name, req, onLoad, config) {
...
url = req.toUrl(nonStripName),
//returns "scripts/**text!**MainTemplate.html**.html**"
...
}
If I name the module, 'MainTemplate.html' it works fine but I'd like to keep the extension out of the module name.
I've modified the text plugin with a simple regex replacement to strip out the plugin reference and the duplicate extension.
Is there a better way to handle this?
Ran into similar issue. Solved finally. See TypeScript: compiling removes unreferenced imports
/// <amd-dependency path="text!templates/application.htm" />
var applicationTemplate = require('text!templates/application.htm');
For Typescript 1.0 this works for me.
First I created a .d.ts file which stores all module declarations for each text template.
//workaround for typescript's lack of support for requirejs text template notation
//remember that a reference to the import variable has to be found in the class, otherwise typescript ignores the import
declare module "text!views/details/details.html" {
var text: string;
export = text;
}
declare module "text!views/layout/layout.html" {
var text: string;
export = text;
}
declare module "text!views/home/home.html" {
var text: string;
export = text;
}
then to refer to the text template I add these lines on top of the class/module.
/// <reference path="../texttemplate.d.ts"/>
import detailsTemplate = require('text!views/details/details.html');
The reference line is not actually needed, since the .d.ts file is picked up globally. But I added it as a reminder of the workaround. It also makes it easy to ctrl+click to go the d.ts. file.
There is a slightly nicer way to do this (I'm using typescript 2.0)
Referenced here: https://www.typescriptlang.org/docs/handbook/triple-slash-directives.html
This code expects that your requirejs configuration and plugins are set up correctly:
/// <amd-dependency path="text!./about.html" name="template"/>
declare let template: string;
This helped me a lot to migrate lagacy code to typescript.
Since TypeScript 0.9.0 I think you need to do the following:
/// <amd-dependency path="text!templates/application.htm" />
declare var require:(moduleId:string) => any;
var applicationTemplate:string = require("text!templates/application.htm");
Check out more at http://www.codebelt.com/typescript/typescript-amd-with-requirejs-tutorial/
We are using Backbone and require.js for our TypeScript applications.
We don't use the
import backbone = module("Backbone")
syntax, but rather use a
/// <reference path="../../modules/Backbone.d.ts" />
reference, and then a BootStrapper.
This way, the 'text!htmlfile.html' syntax works perfectly with require.js.
I've put together a blog on using require.js with TypeScript and AMD:
http://blorkfish.wordpress.com/2012/10/23/typescript-organizing-your-code-with-amd-modules-and-require-js/

Resources