'Isomorphic' __dirname in ESM and CommonJS - node.js

I maintain a library, written in Typescript and I want to target both CommonJS and ESM. To do this I run the build twice. This requires me to write code (in Typescript) that's valid for both.
One issue is that CommonJS has a __dirname variable, which I use to load a file relatively. __dirname doesn't exist in ESM, so I tried to do something like this:
const _mydirname = typeof __dirname !== 'undefined'
? __dirname
: path.dirname(url.fileURLToPath(import.meta.url));
// Example usage
const file = fs.readFileSync(path.join(_mydirname, '../README.md'), 'utf-8')
Unfortunately when building this in Typescript, it emits the following error:
Error: src/application.ts(22,36): error TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', or 'nodenext'.
Is there a way to do get a path relative to the current script that works in both scenarios?

I don't think there is any uniform way to get the current script path for both CommonJS and ESM.
If you are building it twice anyway, you can add some conditional build time logic to generate the proper code for each module system, the actual implementation will depend on your typescript build system. Bundlers like Webpack/Rollup have plugins for that, if you're using plain tsc, you will probably need to implement it yourself.
If you want to keep it simple and rely on runtime assertion of __dirname/import.meta you can just tell typescript to ignore the error:
const _mydirname = typeof __dirname !== 'undefined'
? __dirname
// #ts-ignore
: path.dirname(fileURLToPath(import.meta.url));
As a side note, be careful when basing logic on source script location, it may make sense during development but have different structure when built (e.g if you're using a bundler).

Related

Webpack / Vue.js: generate module code at compile-time using ESM dependencies

Environment: webpack 5.44 + vue.js 3.0 + node 12.21
I'm trying to generate a module at compile-time, in order to avoid a costly computation at run-time (as well as 10Mb of dependencies that will never be used except during said computation). Basically run this at compile-time:
import * as BigModule from "big-module";
function extract_info(module) { ... }
export default extract_info(BigModule);
which will be imported at run-time as:
export default [ /* static info */ ];
I tried using val-loader (latest 4.0) which seems designed exactly for this use case.
Problem: big-module is an ESM, but val-loader apparently only supports CJS. So I can neither import ("Cannot use import statement outside a module" error) nor require ("Unexpected token 'export'" error).
Is there any way to make val-loader somehow load the ESM module? Note that I'm not bent on using val-loader, any other technique that achieves the same goal is just as welcome.
After learning way more than I wanted about this issue and node/webpack internals, there seems to be two possible approaches to import ESM from CJS:
Use dynamic import(). But it is asynchronous which makes it unfit here, as val-loader requires a synchronous result.
Transpile the ESM into CJS, which is the approach I took.
In my case, full transpiling is overkill and rewriting imports/exports is sufficient, so I'm using ascjs to rewrite the ESM files, along with eval to safely evaluate the resulting string.
All in all:
// require-esm.js
const fs = require('fs');
const ascjs = require('ascjs');
const _eval = require('eval');
function requireESM(file) {
file = require.resolve(file);
return _eval(ascjs(fs.readFileSync(file)), file, { require: requireESM }, true);
}
module.exports = requireESM;
// val-loader-target.js
const requireESM = require('./require-esm');
const BigModule = requireESM('big-module');
function extract_info(module) { ... }
module.exports = extract_info(BigModule);
Note that:
ascjs is safe to use on CJS modules, since it only rewrites ESM imports/exports. So it's OK for big-module or its dependencies to require CJS files.
the third argument to _eval enables recursive rewriting, otherwise only the top-level file (the one passed to requireESM) is translated.

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);

Almond.js and define() method. Throws: Uncaught Error: incorrect module build, no module name

The story is:
In our firm we're slowly trying to adjust our huge application to use dynamic AMD module loader. We can't do it all at once, this is why we do it in steps: firstly we wish to rewrite all javascript into typescript using AMD requires and 'fake' modularity using almond.js. After we rewrite everything, we'll switch to real dynamic module loader.
Here comes the problem
When we include almond on the page, following error is thrown:
almond.js:414 Uncaught Error: See almond README: incorrect module build, no module name
at define (almond.js:414)
at plotly.js:7
at plotly.js:7
It comes from several libraries, not just plotly. I managed to track it down, and it turns out that almond introduces define() which takes 3 required parameters, while plotly (and some other libraries) calls define() using one or two of them:
Plotly:
if (typeof define==="function" && define.amd ) {
define([],f)
}
Almond:
define = function (name, deps, callback) {
if (typeof name !== 'string') {
throw new Error('See almond README: incorrect module build, no module name');
}
(...)
Question:
Do you have any idea how to approach to solve this problem? We can load almond.js after Plotly.js, but we'd like to find better solution and use Plotly in conjuction with almond. Is that even possible?
As Almond's README states:
Only useful for built/bundled AMD modules, does not do dynamic loading.
Emphasis added. The form of define without a string giving the module name as the first argument is used in files that have not been through a building or bundling process. An AMD bundler will take the define call and add the module name as the first parameter when it does the bundling. The files you refer to have not been included in a bundle, so they lack the module name.
Solution: use an AMD bundler, like r.js to bundle your modules into a single bundle that Almond will use.

Using node require with Electron and Webpack

I am building a project with Electron, and using Webpack to build the (Angular 2) render process app.
In this app, I need to dynamically require some files at run-time which do not exist at build-time. The code looks something like this:
require("fs").readdirSync(this.path).forEach(file => {
let myModule = require(path.join(this.path, file));
// do stuff with myModule
});
The problem is that the Webpack compiler will convert the require() call to its own __webpack_require__() and at run-time, it will look in its own internal module registry for the dynamic "myModule" file, and of course will not find it.
I have tried using the "externals" config option, but since this is a dynamic require, it doesn't seem to be processed by "externals".
Anyone else had success in solving this problem?
As suggested in a comment to my question by #jantimon, the solution is to use global.require:
require("fs").readdirSync(this.path).forEach(file => {
let myModule = global.require(path.join(this.path, file));
// do stuff with myModule
});
I came across this article and for some other reason the author needs node modules which gets not transpiled by webpack. He suggested to use
new webpack.IgnorePlugin(new RegExp("^(fs|ipc)$"))
in the webpack.config.js file. This should prevent to transpile the module fs and ipc so it can be used (required) in code.
I am not really sure if this hits also your problem but it might help.
The original article for more context can be found here: https://medium.com/#Agro/developing-desktop-applications-with-electron-and-react-40d117d97564#.927tyjq0y

Typescript/RequireJS having issues with an MVC application

I have the following code in Visual Studio, in an MVC application;
/scripts/bin/models/ViewModel.ts
export class ViewModel {
// view model code
}
Now, I have downloaded requirejs, and set the build mode for typescript to AMD type, so that its output looks such as....
define(["require", "exports"], function(require, exports) {
And so on ...
So then I declare my app/config.js file like so;
require.config({
baseUrl: '/scripts/bin'
});
And I try to load this up, I have requirejs loaded into the scripts, and attempt to call it...
require(['models/ViewModel'], function (viewModel) {
console.log("test");
});
And I am simply told that it is an invalid call. No other details. The path that it shows is completely correct, too. Is there some kind of additional configuration required? The requirejs documentation is extremely vague about this.
SOLUTION
This turned out to have nothing to do with requirejs, but instead had to do with IIS.
By default, IIS has a rule known as hiddenSegments. It does not allow you to bring in any code from a folder with bin in the path. I simply renamed the folder from bin to something else, and it worked fine.
Using require.js with TypeScript is a combination of your .html, require.config, module exports and imports.
For a step-by-step guide on moving from CommonJs TypeScript to AMD and require.js, have a look here.
Have fun.
The TypeScript compiler doesn't have any knowledge of your require.config - so when you use paths relative to that baseUrl they look invalid to the compiler.
Until something is done to bridge that slight mismatch (i.e. make the compiler super-clever so it can look for require.config sections and use them to check paths) it is easier not to set a baseUrl and use the full path in your import statements:
import vm = require('./scripts/bin/models/ViewModel');
Are you sure that the require call is done with [] and not just
require('models/ViewModel', function (viewModel) { // this is an error
console.log("test");
});
See : http://requirejs.org/docs/errors.html#requireargs

Resources