Importing JSX with ESM Dynamic Imports in Node.js - node.js

Summary of problem
I'm exclusively using ESM in my Node.js project and trying to find a way to dynamically import JSX.
I'm making a custom static site generator for my website and want to render React components to markup with renderToStaticMarkup(), but to achieve this, I first need to successfully import the components to then run this method.
Does anyone know a way to dynamically import JSX in ESM Node.js?
I.e., to make await import("./jsxComponent.js") work?
A few things I've tried
Approach 1: Direct attempt
When I dynamically import the .js file containing the component, I receive the error message: SyntaxError: Unexpected token '<'. Seems import() cannot parse JSX out of the box.
If I change the file extension of the .js file to .jsx, I unsurprisingly receive the error message Unknown file extension ".jsx".
Approach 2: Babeling
Back in the CommonJS heyday of Node.js, I'd use #babel/register, #babel/preset-env, and #babel/preset-react in a separate file with its last line invoking require() on another .js file that, inside itself, would then require() the component. I'm not entirely clued up on how each Babel preset or plugin works, but this did the trick back then allowing me to require() components to then render them to markup. Unfortunately, this doesn't work when using ESM-only packages in an ESM-only project because the moment I use #babel/register my ESM-only packages complain and break.
I've tried using #babel/core to transform the file before it's invoked inside import(). I've done this by using the transformFileSync method, but this created the error message: Error [ERR_MODULE_NOT_FOUND]: Cannot find package '"use strict". Inside the options object of transformFileSync I used babel-plugin-dynamic-import-node as a plugin and #babel/register, #babel/preset-env, and #babel/preset-react as presets.
I've tried also using #babel/core's transformSync method by passing in the JSX code directly (rather than just the file path of the JSX-containing file), and this created the error message: Error: ENOENT: no such file or directory, open 'import Header from "./src/components/header.js"; (note: there IS a file at ./src/components/header.js - it is one of the components being imported inside another component.)
Approach 3: Require
Other approaches online recommend using require() instead of import(), but as I said, this is an ESM-only project using ESM-only packages and so the error message I receive when trying this is require is not defined, as one would expect.
Code examples
Approach 1: Direct attempt
const module = await import("./jsxComponent.js")
Approach 2: Babeling
const module = await import(
babelCore.transformFileSync("./jsxComponent.js", {
presets: [
"#babel/preset-env",
[
"#babel/preset-react",
{
runtime: "automatic",
},
],
],
plugins: ["dynamic-import-node"],
}).code
);
(Let me know if you want me to post any more code examples from my tests with Babel).
Approach 3: Require
const module = require("./jsxComponent.js")

I was able to import JSX in my ESM-only project by:
Installing #node-loader/babel (see GitHub repo)
Installing #babel/core and #babel/preset-react
Creating babel.config.js in my root directory with the following setup:
export default {
presets: [
[
"#babel/preset-react",
{
runtime: "automatic",
},
],
],
};
Then running my node build script with the node loader set as an experimental loader: node --experimental-loader #node-loader/babel ./lib/build.js
I was then able to successfully use const component = await import("./jsxComponent.js") in my node build scripts and pass the component to reactDOMServer's renderToStaticMarkup(component()) method by invoking it as a function.

Related

Using ipcRenderer in Angular 15 + Electron 22 project causes building errors

I am trying to use ipcRenderer in an Angular service. My project will only work in an Electron environment. If I try to use the standard builder, I get the following error:
BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default.
This is no longer the case. Verify if you need this module and configure a polyfill for it.
If you want to include a polyfill, you need to:
- add a fallback 'resolve.fallback: { "path": require.resolve("path-browserify") }'
- install 'path-browserify'
If you don't want to include a polyfill, you can use an empty module like this:
resolve.fallback: { "path": false }
If I use the esbuild builder, this is what I get:
X [ERROR] Could not resolve "fs"
node_modules/electron/index.js:1:19:
1 │ const fs = require('fs');
╵ ~~~~
The package "fs" wasn't found on the file system but is built into node. Are you trying to bundle for node? You can use "platform: 'node'" to do that, which will remove this error.
I tried to put
"platform: 'node'"
Both in tsconfig file and angular.json file, but it seems like it is not a valid option.
If I add this block to package.json:
"browser": {
"path": false,
"fs": false
}
Then I have the following error at runtime:
ERROR Error: Uncaught (in promise): ReferenceError: __dirname is not defined
Even though I haven't used __dirname anywhere in the browser code.
What should I do to make it work?
Electron has two separate processes - the render process, which runs in the bundled web browser (the ipcRenderer), and the main process, which runs in a node.js process.
As such, you can use core node.js modules in the main process, but not in the ipcRenderer since these modules (e.g. fs) are not available in the browser.
It sounds like you're trying to use fs in the renderer (your Angular app). That's not going to work.
If you want to, for example, read data from the file system and display it in the browser, you're going to have to use Electron's interprocess communications to have the Angular ask the main process to read the file, and then pass the content back to the renderer from the main process asynchronously.

Cannot use import statement outside a module with #pusher/push-notifications-web nodejs - beams

I am trying to follow this tutorial using nodejs and express: https://pusher.com/docs/beams/reference/web/#npm-yarn
First I did: npm install #pusher/push-notifications-web before adding the code.
But when I add this code in the index.js file:
import * as PusherPushNotifications from "#pusher/push-notifications-web";
const beamsClient = new PusherPushNotifications.Client({
instanceId: "<YOUR_INSTANCE_ID_HERE>",
});
beamsClient.start().then(() => {
// Build something beatiful 🌈
});
I get this error:
SyntaxError: Cannot use import statement outside a module
It's also not very clear to me from the tutorial if the code has to be in the frontend or the backend. I tried both but got the same result.
How can I fix this problem?
The error is caused by the fact that you're trying to use ES module specific features in a regular CommonJS file (the default behavior in Node.js). However, what you're looking at is the Web SDK for Pusher which won't help you achieve your goals.
You need the server SDK for Node.js - https://pusher.com/docs/beams/reference/server-sdk-node/.
Verify that you have the latest version of Node.js installed and you have 2 ways of fixing that
Set "type" field with a value of "module" in package.json. This will ensure that all .js and .mjs files are interpreted as ES modules.
// package.json
{
"type": "module"
}
Use .mjs as file extension instead of .js.

Error: Identifier 'Reader' has already been declared while bundling fstream with rollup.js

I'm new with configuring stuff with Rollup, I'm trying to configure a node CLI app to be bundled in a single cli.bundle.js file that would be executable with a simple node cli.bundle.js without needing to npm install anything before.
On a side note, we already have succeed in doing so with zeit/pkg, but we would rather note have all the nodejs executable bundled inside, so we are trying work with rollup instead of pkg.
The problem we encounter is that when going through the different node_modules of the application, rollup.js crash with error:
[!] Error: Identifier 'Reader' has already been declared
../../common/js-common/node_modules/fstream/lib/file-reader.js (7:4)
5: var fs = require("graceful-fs")
6: , fstream = require("../fstream.js")
7: , Reader = fstream.Reader
^
8: , inherits = require("inherits")
9: , mkdir = require("mkdirp")
Error: Identifier 'Reader' has already been declared
at error (/home/.../src/external-data/external-data-etl/node_modules/rollup/dist/shared/node-entry.js:5400:30)
at Module.error (/home/.../src/external-data/external-data-etl/node_modules/rollup/dist/shared/node-entry.js:9820:16)
at tryParse (/home/.../src/external-data/external-data-etl/node_modules/rollup/dist/shared/node-entry.js:9713:23)
at Module.setSource (/home/.../src/external-data/external-data-etl/node_modules/rollup/dist/shared/node-entry.js:10076:33)
at Promise.resolve.catch.then.then.then (/home/.../src/external-data/external-data-etl/node_modules/rollup/dist/shared/node-entry.js:12362:20)
While looking for this error with rollup, it seems people were having it more at execution time than at bundle time, so I have no clue of what I can do. This duplicated identifier is in a 3rd party code I don't control :(
Here is my rollup.config.js
Edit: I tried with the new #rollup/plugins to see if there were a fix in them, but I have still the same issue.
import commonjs from '#rollup/plugin-commonjs';
import resolve from '#rollup/plugin-node-resolve';
import json from '#rollup/plugin-json';
export default {
input: 'dist/index.js',
output: {
format: 'cjs',
file: './cli.bundle.js'
},
plugins: [
commonjs(),
resolve(),
json() // asked and added when parsing 'got' package imported by 'download' package
]
};
And our building process is :
transpile from typescript (src) to js (dist) with tsc
bundle (dist) app into single runnable file
We would rather not include babel or typescript plugin to transpile, to stay independant, and certainly the (dist) app is enough (as it was enough for zeit/pkg).
Is there something we are doing wrong ?
The Problem is your dependency it is not coded in the right way you will need to correct the code error they did.

How to import a node module inside an angular web worker?

I try to import a node module inside an Angular 8 web worker, but get an compile error 'Cannot find module'. Anyone know how to solve this?
I created a new worker inside my electron project with ng generate web-worker app, like described in the above mentioned ng documentation.
All works fine until i add some import like path or fs-extra e.g.:
/// <reference lib="webworker" />
import * as path from 'path';
addEventListener('message', ({ data }) => {
console.log(path.resolve('/'))
const response = `worker response to ${data}`;
postMessage(response);
});
This import works fine in any other ts component but inside the web worker i get a compile error with this message e.g.
Error: app/app.worker.ts:3:23 - error TS2307: Cannot find module 'path'.
How can i fix this? Maybe i need some additional parameter in the generated tsconfig.worker.json?
To reproduce the error, run:
$ git clone https://github.com/hoefling/stackoverflow-57774039
$ cd stackoverflow-57774039
$ yarn build
Or check out the project's build log on Travis.
Note:
1) I only found this as a similar problem, but the answer handles only custom modules.
2) I tested the same import with a minimal electron seed which uses web workers and it worked, but this example uses plain java script without angular.
1. TypeScript error
As you've noticed the first error is a TypeScript error. Looking at the tsconfig.worker.json I've found that it sets types to an empty array:
{
"compilerOptions": {
"types": [],
// ...
}
// ...
}
Specifying types turns off the automatic inclusion of #types packages. Which is a problem in this case because path has its type definitions in #types/node.
So let's fix that by explicitly adding node to the types array:
{
"compilerOptions": {
"types": [
"node"
],
// ...
}
// ...
}
This fixes the TypeScript error, however trying to build again we're greeted with a very similar error. This time from Webpack directly.
2. Webpack error
ERROR in ./src/app/app.worker.ts (./node_modules/worker-plugin/dist/loader.js!./src/app/app.worker.ts)
Module build failed (from ./node_modules/worker-plugin/dist/loader.js):
ModuleNotFoundError: Module not found: Error: Can't resolve 'path' in './src/app'
To figure this one out we need to dig quite a lot deeper...
Why it works everywhere else
First it's important to understand why importing path works in all the other modules. Webpack has the concept of targets (web, node, etc). Webpack uses this target to decide which default options and plugins to use.
Ordinarily the target of a Angular application using #angular-devkit/build-angular:browser would be web. However in your case, the postinstall:electron script actually patches node_modules to change that:
postinstall.js (parts omitted for brevity)
const f_angular = 'node_modules/#angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js';
fs.readFile(f_angular, 'utf8', function (err, data) {
var result = data.replace(/target: "electron-renderer",/g, '');
var result = result.replace(/target: "web",/g, '');
var result = result.replace(/return \{/g, 'return {target: "electron-renderer",');
fs.writeFile(f_angular, result, 'utf8');
});
The target electron-renderer is treated by Webpack similarily to node. Especially interesting for us: It adds the NodeTargetPlugin by default.
What does that plugin do, you wonder? It adds all known built in Node.js modules as externals. When building the application, Webpack will not attempt to bundle externals. Instead they are resolved using require at runtime. This is what makes importing path work, even though it's not installed as a module known to Webpack.
Why it doesn't work for the worker
The worker is compiled separately using the WorkerPlugin. In their documentation they state:
By default, WorkerPlugin doesn't run any of your configured Webpack plugins when bundling worker code - this avoids running things like html-webpack-plugin twice. For cases where it's necessary to apply a plugin to Worker code, use the plugins option.
Looking at the usage of WorkerPlugin deep within #angular-devkit we see the following:
#angular-devkit/src/angular-cli-files/models/webpack-configs/worker.js (simplified)
new WorkerPlugin({
globalObject: false,
plugins: [
getTypescriptWorkerPlugin(wco, workerTsConfigPath)
],
})
As we can see it uses the plugins option, but only for a single plugin which is responsible for the TypeScript compilation. This way the default plugins, configured by Webpack, including NodeTargetPlugin get lost and are not used for the worker.
Solution
To fix this we have to modify the Webpack config. And to do that we'll use #angular-builders/custom-webpack. Go ahead and install that package.
Next, open angular.json and update projects > angular-electron > architect > build:
"build": {
"builder": "#angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "./extra-webpack.config.js"
}
// existing options
}
}
Repeat the same for serve.
Now, create extra-webpack.config.js in the same directory as angular.json:
const WorkerPlugin = require('worker-plugin');
const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
module.exports = (config, options) => {
let workerPlugin = config.plugins.find(p => p instanceof WorkerPlugin);
if (workerPlugin) {
workerPlugin.options.plugins.push(new NodeTargetPlugin());
}
return config;
};
The file exports a function which will be called by #angular-builders/custom-webpack with the existing Webpack config object. We can then search all plugins for an instance of the WorkerPlugin and patch its options adding the NodeTargetPlugin.

how to use node module with es6 import syntax in typescript

I have a typescript project which has uses one of our node modules which normally runs in our front-end. We are now looking to use this module in node on our server.
The module uses es6 import syntax import { props } from 'module/file'
When I include a ref in typescript using either of the following methods
import { props } from 'module/file';
var props = require('module/file');
I get the following error from typescript
unexpected token 'import'
(function (exports, require, module, __filename, __dirname) { import
It's a big job to re-write the module, and I've tried using babel with babel-plugin-dynamic-import-node, as well as SystemJS.
The problem with these systems is that they are all asynchronous, so I can't import the module in the standard fashion, so I would need to do a whole bunch of re-write when we get to the point that I can use import natively in node.js.
I can't be the first person to have this issue, but I can't seem to find a working solution.
--------------- update with set-up -------------
In response to #DanielKhoroshko's response. The original module I am trying to import is normally packaged by webpack in order to use on the front-end. I am now trying to use this same module both server-side and in the front-end (via webpack on the front-end) without re-writing the imports to use require and without running webpack to bundle the js to use on the server.
To be clear, the original module is written in JS, our service which is trying to use this module is written in typescript and transpiled. When the typescript tries to require the old module which uses import, it is at this point that we are running into the issue.
------------------ some progress ---------------------------
I've made some progress by creating a file in my imported module which uses babel in node.js to transpile the es6 code into commonJS modules.
I've done this via
var babel = require("babel-core")
var store = babel.transformFileSync(__dirname + '/store.js', {
plugins: ["transform-es2015-modules-commonjs"]
});
module.exports = {
store: store.code
}
I can now get the store in my new node.js project. However, the submodules within the store.js file are not included in the export.
So where in my module, it says
import activities from './reducers/activities';
I now get an error
Cannot find module './reducers/activities'
How can I get babel to do a deep traversal to include the sub-directories?
unexpected token 'import' means you are running es-modules code in environment that doesn't support import/export commands. If you are writing you code in TypeScript it's important to transpile it first before building for the browser or use ts-node to run it server-side.
If you are using webpack there are loaders ts-loader and awesome-typescript-loader
What is your setup?
To describe the module you would need to create an activities.d.ts file in the same folder where the js-version (I understood it is called activities.js and containers a reducer) resides with the following (approx.):
import { Reducer } from 'redux';
export const activities: Reducer<any>;
#Daniel Khoroshko was right in many ways, I ended up finding #std/esm which lets you import es6 modules and worked find for fetching the included imports as well.
var babel = require('babel-register')({
presets: ["env"]
});
require = require('#std/esm')(module);
var store = require('ayvri-viewer/src/store');
exports.default = {
store: store
}
I had to run babel to get a consistent build from es6 to node compatible es5

Resources