In Webpack, how do I set the public path dynamically? - node.js

I am using Angular2/Typescript/Webpack to build an application
I'm having problem setting the Webpack option publicPath dynamically. In the official Webpack docs, it says:
Note: In cases when the eventual publicPath of output files isn't known at compile time, it can be left blank and set dynamically at runtime in the entry point file. If you don't know the publicPath while compiling you can omit it and set __webpack_public_path__ on your entry point.
My question: But how do I set this __webpack_public_path__ variable and where?
I thought I would have to set it in src/main.ts, but then I just get compiler error ERROR in ./src/main.ts
Cannot find name '__webpack_public_path__' when I build the project:
Isn't main.ts where I should set this variable? I even tried to set it in the built version of the file, main.js, and that didn't work either. Here is part of my Webpack config where I set the entry point.
config.entry = isTest ? {} : {
'polyfills': './src/polyfills.ts',
'vendor': './src/vendor.ts',
'app': './src/main.ts' // our angular app
};

compiler error ERROR in ./src/main.ts
TypeScript compiler errors are mostly just really powerful linting. More on this.
That compiler errors just tells you the TypeScript doesn't know about __webpack_public_path__. Just create globals.d.ts with :
declare var __webpack_public_path__:string;
And you should be golden. More on this 🌹

Related

eslint complains about __dirname not being defined in a NodeJS file

I've just started using eslint. I initialized it and started fixing problems it pointed out.
One problem, however, it shouldn't complain about:
const data = fs.readFileSync(path.join(__dirname, _FILENAME));
The error is:
error '__dirname' is not defined
I did some searching and found that when you add --experimental to the node command that __dirname is not defined. This, however, isn't the case for me. I'm not running node with the --experimental flag.
See these questions:
Alternative for __dirname in Node.js when using ES6 modules
__dirname is not defined in nodejs
This is happening because ESLint does not know that your code is supposed to be used in Node.js: __dirname is not defined in browsers and also not defined in ES modules. To tell ESLint that your code will run in Node.js as a CommonJS module, open your ESLint config and set node: true in the env section. If you are using .eslintrc.json:
{
"env": {
"node": true
}
}
There are also other ways to specify environments, they are explained in the related documentation.
I ran into the same. Adding this to .eslintrc.json fixed it for me:
"globals": {
"__dirname": true
}
Got the idea from this similar question about the process global: https://stackoverflow.com/a/56777068/936907

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 can I keep "require" working for dynamic expressions

I want to bundle a Node.js script, which somewhere calls require(expression). After bundling the script with webpack, require fails. This is a super simplified example:
// main.js
const x = require(process.argv[2])
console.log(x)
I would like to either have a "normal" require for this case or tell webpack to include a specific file which I know will be required in the future (after bundling). To stick with this example, I know the value of process.argv[2] ahead of bundling.
Note: The code doing the expression based require is a dependency, so I cannot tweak require code.
This is my webpack.config.js
module.exports = {
entry: './test.js',
output: {
filename: 'test.js'
},
target: 'node'
}
The require path is relative to the file it is used in. So you'll need to figure out the path from where require is executing to the file it's loading from the parameter. Then prepend the relative part to the parameter.

Typescript2 path module resolution

tl;dr : module resolution does not apply ?
Hello,
I am playing around with Typescript2 module resolution feature.
I've noticed that it is now possible to specify "Paths", so that you can do the following :
Old way
import {a} from "../../../foo"
New way
import {a} from "services/foo"
To do so, you need to add some configs to your tsconfig.json
"compilerOptions": {
"baseUrl": ".",
"paths": {
"services/*": ["./application/core/services/*"],
}
}
Problem that I have, is that when compiled, the import actually doesn't change. My javascript output still contains that import from "services/foo", so that obviously crash at runtime on my node server.
I use gulp-typescript to compile my javascript files :
var tsProject = ts.createProject("tsconfig.json");
return tsProject.src()
.pipe(sourcemaps.init())
.pipe(tsProject()).js
.pipe(sourcemaps.write("../api"))
.pipe(gulp.dest(function(file) {
return file.base;
}));
I am completely lost here and would love to use that module resolution, so that I can move away from that ../../ hell of imports. Any help would be more than appreciated !
The problem here is that the JavaScript Engine knows nothing about your TypeScript config, what you specify in your tsconfig is only used "compile time", when you have compiled your TypeScript into JS you need to do the same job as the TS compiler did but to save the resolved path in the JS file.
Simply put, all JS files needs to be processed and the aliases replaced with "real" paths.
Tip: Use the npm tool tspath (https://www.npmjs.com/package/tspath), it requires 0 configuration, just run it in somewhere in your project and all JS files will be processed and ready to run!

Will env. vars affect imports from node_modules?

I'm extracting some utility functions from my project into npm modules so I can use them in other projects.
Some of these functions rely on API Keys, and use different API Keys for DEV and PROD environments.
In the main project, this isn't a problem, as my build process sets the environment vars correctly, and my webpack build process results in the correct keys in the source code.
If I include the API Keys in the npm modules with the same DEV | PROD ternaries, will the environment vars of the main project work the same for the modules as they did for the code when it was in the main project?
// mymodule/index.js
export default function() {
return __PROD__ === true ? "abc" : "123"
// ....
}
// project/index.js in __DEV__ environment
import getKey from 'my-module'
getKey() // should return "123"
I may be misunderstanding your question, but what you're using aren't environment variables. They're probably variables you're filling in with Webpack's DefinePlugin, such as
new webpack.DefinePlugin({
__PROD__: true
})
When Webpack compiles your code, the plugin automatically replaces these magic global variables with the value in the configuration file.
The correct way to use an environment variable in node is process.env, as in process.env.__PROD__, and run your program with something like __PROD__=true node index.js, or by the more common convention NODE_ENV=production node index.js.
If you're publishing the compiled Webpack code to npm, then it should work with whatever DefinePlugin value you've set for the npm compile Webpack configuration.

Resources