Why converting ESM to CommonJS is not simple find and replace? - node.js

I've been working on a TypeScript project (for NodeJs environment) so I've been using ES module syntax for imports and exports. Using TSC with "module": "commonjs", "esModuleInterop": true, there is a lot of boilerplate code created such as:
var __importStar = (this && this.__importStar) || function (mod) {
// omitted
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = require("path"); // renamed identifier
const pug_1 = __importDefault(require("./template/pug"));
pug_1.default(...) // use of .default
So my question is, why we cannot simply convert ESM import/exports to plain require calls for NodeJs and why this boilerplate code and identifier renaming are needed?
For example, why following conversions cannot be done by a simple find-and-replace (with regex or some parsing):
import * as path from "path";
// const path = require("path");
import { resolve } from "path";
// const { resolve } = require("path");
export default class MyClass {...}
// module.exports = class MyClass {...}
export class MyClass {...}
// module.exports.MyClass = class MyClass {...}

It's not a 1:1 mapping. For example, using the ES6 import/export syntax, you can export multiple symbols and a default symbol. To do the same thing in CommonJS, you'd need to start nesting the exported objects into other exported objects which could cause problems.

Related

Cannot use import after adding Typescript

So I'm trying to implement typescript to an existing project.
However, I came to a stop, where I get an error of: SyntaxError: Cannot use import statement outside a module
Here, is my helper class, which is omitted. However, you can see that I am using an import, rather than require
index.ts
// const axios = require('axios');
// const {includes, findIndex} = require('lodash');
// const fs = require('fs');
import { includes, findIndex } from "lodash";
import fs from 'fs';
type storeType = {
[key: string]: string | boolean
}
class CMS {
_store;
constructor(store: storeType) {
this._store = store;
<omitted code>
export default CMS;
}
Than, I import index.ts file to server.js file:
const { CMS, getCookie, checkLang, getLangByDomain, handleRoutes } = require('./src/utils/cms/index.ts');
Unfortunately, when I start the server, I get an error of: SyntaxError: Cannot use import statement outside a module
I am using a default tsconfig.json which has been generated after creating file and running dev environment.
Edit your tsconfig.json and change "module": "esnext" to "module": "commonjs".
This is ES type modules:
import { includes, findIndex } from "lodash";
import fs from 'fs';
But this is commonJs type:
const { CMS, getCookie, checkLang, getLangByDomain, handleRoutes } =
require('./src/utils/cms/index.ts');
I think that's the problem. You should use one type of modules.
Try to rewrite this const { CMS, getCookie, checkLang, getLangByDomain, handleRoutes } = require('./src/utils/cms/index.ts'); to this import { CMS, getCookie, checkLang, getLangByDomain, handleRoutes } from './src/utils/cms/index.ts'
Or opposite rewrite ES to commonJs, but don't forget to change type in tsconfig
You cannot explicitly import typescript files into Javascript files. Instead, you need to use the compiled typescript files(i.e. Javascript files in outDir folder).
So assume you compiled your typescript files into Javascript, then it would be converted to outDir/index.js. After that, you could directly import it into server.js
const { CMS, getCookie, checkLang, getLangByDomain, handleRoutes } =
require('./path/to/index.js'); // You cannot require a ts file.
If the typescript files and Javascript files are part of the same project, then you need to transpile the js files alongside the ts as well. In order to achieve this, you need to set allowJs to true in tsconfig.
{
"compilerOptions": {
...
"allowJs": true,
}
}
Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files.

Cannot redeclare exported variable ''

I have a npm module in another directory that I'm using in another project using npm link, however when I import the module and try to use the function I get the following error a bunch of errors even though the typescript compiled successfully. Here is my tsconfig for the npm module:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"outDir": "./lib",
"strict": true,
},
"include": ["src"],
"exclude": ["node_modules"]
}
Here is the index.ts of the module:
import * as APIServices from "./API/API"
import APIPage from "./API/APIPage"
export { APIServices, APIPage }
And here is how I'm trying to use the package:
import APIServices from 'common-backend'
console.log(APIServices)
But when I run the file I get the following errors:
TSError: тип Unable to compile TypeScript:
../../common-backend/lib/index.js:3:1 - error TS2323: Cannot redeclare exported variable 'APIPage'.
3 exports.APIPage = exports.APIServices = void 0;
~~~~~~~~~~~~~~~
../../common-backend/lib/index.js:3:19 - error TS2323: Cannot redeclare exported variable 'APIServices'.
3 exports.APIPage = exports.APIServices = void 0;
~~~~~~~~~~~~~~~~~~~
../../common-backend/lib/index.js:5:1 - error TS2323: Cannot redeclare exported variable 'APIServices'.
5 exports.APIServices = APIServices;
~~~~~~~~~~~~~~~~~~~
../../common-backend/lib/index.js:7:1 - error TS2323: Cannot redeclare exported variable 'APIPage'.
7 exports.APIPage = APIPage_1.default;
Finally this is the index.js that the errors are being thrown on:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.APIPage = exports.APIServices = void 0;
var APIServices = require("./API/API");
exports.APIServices = APIServices;
var APIPage_1 = require("./API/APIPage");
exports.APIPage = APIPage_1.default;
Is there something that I forgot to add in the tsconfig? Is this the fault of the compiler transpiling the typescript incorrectly? Or does it have to do something with the way I'm import and exporting the files? I've been scratching my head for a while on this one and nobody else seems to have had the same issue.
Edit:
Forgot to include the API class I'm trying to import:
import { Validators } from "./Validators"
import { APIRoute, Config } from "./helpers/Interface"
import { Router } from "express"
const router = Router()
class API {
private config: Config
private _routes: APIRoute[]
constructor(config: Config) {
this.config = config
this._routes = []
}
get routes() : APIRoute[] {
return this._routes
}
set routes(routes: APIRoute[]) {
this._routes = [...routes]
}
/**
* Add route to routes array
* #param route
*/
addRoute(route: APIRoute) : void {
this.routes.push(route)
}
/**
* Instatiates existing routes in the routes array
*/
loadRoutes() : void {
for(const route of this._routes) {
try {
new route.module(router, route, this.config.authFunction)
}
catch(error) {
console.error(route)
console.error(error)
}
}
}
}
export { API, Validators, router }
I don't think will solve all your problems but your imports seem off. There are two ways of handling imports.
export default APIServices
And then you can import like you have
import APIServices from 'common-backend'
Whereas if you want to export multiple named things like you have
export { APIServices, APIPage }
You need to import them one of two ways
import { APIServices } from 'common-backend'
Or I think you can do something like this but I am less familiar with it because I personally don't like how it looks (I much rather just import what I need)
import * as CommonBackend from 'common-backend'
// then use CommonBackend.APIServices
So it turns out the issue was that I was using ts-node to compile and run the code in the project using the package. I changed node --inspect -r ts-node/register src/app.ts" to simply node ./dist/src/app.js and that solved everything. Although not sure what underlying elements were causing the javascript not to be run properly.

How to write a type definition file for a default exported class?

I'm trying to write a type definition file for OpenSubtitles.org api node wrapper. Here is the main file index.js. On line 7 the OpenSubtitles class is exported as module's default export.
module.exports = class OpenSubtitles {
....
}
So i came up with the following
declare module "opensubtitles-api" {
export default class OpenSubtitles {
}
}
This is the transpilation of a code using OpenSubtitles.org api node wrapper and my .d.ts file.
"use strict";
exports.__esModule = true;
var opensubtitles_api_1 = require("opensubtitles-api");
var os = new opensubtitles_api_1["default"]({
useragent: "TemporaryUserAgent"
});
and when i run it. I get this error.
var os = new opensubtitles_api_1["default"]({
^
TypeError: opensubtitles_api_1.default is not a constructor
When i remove the ["default"] part of the transpiled code everying work as expectd.
Desired transpilation
"use strict";
exports.__esModule = true;
var opensubtitles_api_1 = require("opensubtitles-api");
var os = new opensubtitles_api_1({
useragent: "TemporaryUserAgent"
});
How should i export/declare OpenSubtitles class?
Default exports are different from when you are replacing the whole export object. The syntax for that is :
declare module "opensubtitles-api" {
class OpenSubtitles {
}
export = OpenSubtitles
}

module.exports vs. export default in Node.js and ES6

What is the difference between Node's module.exports and ES6's export default? I'm trying to figure out why I get the "__ is not a constructor" error when I try to export default in Node.js 6.2.2.
What works
'use strict'
class SlimShady {
constructor(options) {
this._options = options
}
sayName() {
return 'My name is Slim Shady.'
}
}
// This works
module.exports = SlimShady
What doesn't work
'use strict'
class SlimShady {
constructor(options) {
this._options = options
}
sayName() {
return 'My name is Slim Shady.'
}
}
// This will cause the "SlimShady is not a constructor" error
// if in another file I try `let marshall = new SlimShady()`
export default SlimShady
The issue is with
how ES6 modules are emulated in CommonJS
how you import the module
ES6 to CommonJS
At the time of writing this, no environment supports ES6 modules natively. When using them in Node.js you need to use something like Babel to convert the modules to CommonJS. But how exactly does that happen?
Many people consider module.exports = ... to be equivalent to export default ... and exports.foo ... to be equivalent to export const foo = .... That's not quite true though, or at least not how Babel does it.
ES6 default exports are actually also named exports, except that default is a "reserved" name and there is special syntax support for it. Lets have a look how Babel compiles named and default exports:
// input
export const foo = 42;
export default 21;
// output
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
var foo = exports.foo = 42;
exports.default = 21;
Here we can see that the default export becomes a property on the exports object, just like foo.
Import the module
We can import the module in two ways: Either using CommonJS or using ES6 import syntax.
Your issue: I believe you are doing something like:
var bar = require('./input');
new bar();
expecting that bar is assigned the value of the default export. But as we can see in the example above, the default export is assigned to the default property!
So in order to access the default export we actually have to do
var bar = require('./input').default;
If we use ES6 module syntax, namely
import bar from './input';
console.log(bar);
Babel will transform it to
'use strict';
var _input = require('./input');
var _input2 = _interopRequireDefault(_input);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
console.log(_input2.default);
You can see that every access to bar is converted to access .default.
Felix Kling did a great comparison on those two, for anyone wondering how to do an export default alongside named exports with module.exports in nodejs
module.exports = new DAO()
module.exports.initDAO = initDAO // append other functions as named export
// now you have
let DAO = require('_/helpers/DAO');
// DAO by default is exported class or function
DAO.initDAO()
You need to configure babel correctly in your project to use export default and export const foo
npm install --save-dev #babel/plugin-proposal-export-default-from
then add below configration in .babelrc
"plugins": [
"#babel/plugin-proposal-export-default-from"
]

How do you write a node module using typescript?

So, the general answer for the other question (How do you import a module using typescript) is:
1) Create a blah.d.ts definition file.
2) Use:
/// <reference path="./defs/foo/foo.d.ts"/>
import foo = require("foo");
Critically, you need both the files foo.d.ts and a foo.js somewhere in your node_modules to load; and the NAME foo must exactly match for both. Now...
The question I would like to have answered is how to you write a typescript module that you can import that way?
Lets say I have a module like this:
- xq/
- xq/defs/Q.d.ts
- xq/index.ts
- xq/base.ts
- xq/thing.ts
I want to export the module 'xq' with the classes 'Base' from base.ts, and 'Thing' from thing.ts.
If this was a node module in javascript, my index.ts would look something like:
var base = require('./base');
var thing = require('./thing');
module.exports = {
Base: base.Base,
Thing: thing.Thing
};
Let's try using a similar typescript file:
import base = require('./base');
export module xq {
export var base = base.Base;
}
Invoke it:
tsc base.ts index.ts things.ts ... --sourcemap --declaration --target ES3
--module commonjs --outDir dist/xq
What happens? Well, we get our base.d.ts:
export declare class Base<T> {
...
}
and the thrillingly unuseful index.d.ts:
export declare module xq {
var Base: any; // No type hinting! Great. :(
}
and completely invalid javascript that doesn't event import the module:
(function (xq) {
xq.base = xq.base.Base;
})(exports.xq || (exports.xq = {}));
var xq = exports.xq;
I've tried a pile of variations on the theme and the only thing I can get to work is:
declare var require;
var base = require('./base');
export module xq {
export var base = base.Base;
}
...but that obviously completely destroys the type checker.
So.
Typescript is great, but this module stuff completely sucks.
1) Is it possible to do with the built in definition generator (I'm dubious)
2) How do you do it by hand? I've seen import statements in .d.ts files, which I presume means someone has figured out how to do this; how do those work? How do you do the typescript for a module that has a declaration with an import statement in it?
(eg. I suspect the correct way to do a module declaration is:
/// <reference path="base.d.ts" />
declare module "xq" {
import base = require('./base');
module xq {
// Some how export the symbol base.Base as Base here
}
export = xq;
}
...but I have no idea what the typescript to go along that would be).
For JavaScript :
var base = require('./base');
var thing = require('./thing');
module.exports = {
Base: base.Base,
Thing: thing.Thing
};
TypeScript :
import base = require('./base');
import thing = require('./thing');
var toExport = {
Base: base.Base,
Thing: thing.Thing
};
export = toExport;
Or even this typescript:
import base = require('./base');
import thing = require('./thing');
export var Base = base.Base;
export var Thing = thing.Thin;
Typescript has really improved since this question was asked. In recent versions of Typescript, the language has become a much more strict superset of Javascript.
The right way to import/export modules is now the new ES6 Module syntax:
myLib.ts
export function myFunc() {
return 'test'
}
package.json
{
"name": "myLib",
"main": "myLib.js",
"typings": "myLib.d.ts"
}
Dependents can then import your module using the new ES6 syntax:
dependent.ts
import { myFunc } from 'myLib'
console.log(myFunc())
// => 'test'
For a full example of a node module written in Typescript, please check out this boilerplate:
https://github.com/bitjson/typescript-starter/

Resources