How to add a compile-time global variable in webpack? - javascript

I've got a dynamically configurable webpack setup, with some custom loaders. Each of these loaders needs a configuration object which is only known dynamically, at compile time, so it's not possible to hard code it in loader's options. How can loaders access this object? Is there a "canonical" way to have a compile-time global in webpack?
Basically, my setup allows for multiple dynamic build-time configurations, it looks like this:
// webpack.config.js
let defaults = {
....
module: {
loaders: [
{
test: '.some.special.stuff',
loader: 'my-own-loader',
}
}
module.exports = function main() {
let BUILD_CONFIG = require(process.env.BUILD_CONFIG);
....
return defaults;
}
So webpack is supposed to be called like BUILD_CONFIG=some-config.js webpack. Note that some-config.js contains a lot of non-webpack stuff, so I can't just return it from main or merge it with defaults
Now, I have my-own-loader in my-own-loader.js:
// my-own-loader.js
module.exports = function main(content) {
...
My question is how to access that BUILD_CONFIG variable from webpack's main in the loader's main.

I think I understand what you are trying to do and have an example project detailing the process.
To start, I'm going to set an environment variable named TEST_VAR through whatever mechanism my platform supports. I'm on macOS, so that's export TEST_VAR=fizzbuzz. My loader.js is able to use that environment variable through the node convention: process.env.TEST_VAR.
Remember, your webpack.config.js is still a node run JavaScript file. You can use all the built-ins that node supports including process.env.
Consider the following files and structure:
Structure
webpack-test
|- package.json
|- webpack.config.js
|- index.html
|- /dist
|- bundle.js
|- /loaders
| obj-loader.js
|- /res
|- /obj
|- dummy.obj
|- /src
|- index.js
package.json
{
"name": "webpack-test",
"version": "0.0.1",
"main": "index.js",
"dependencies": {
"webpack": "^3.8.1"
},
"devDependencies": {
"http-server": "^0.10.0",
"loader-utils": "^1.1.0",
},
"scripts": {
"start": "http-server .",
"build": "webpack",
},
}
webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist")
},
resolveLoader: {
modules: [
path.resolve(__dirname, "loaders")
]
},
module: {
rules: [
{
test: /\.obj$/,
use: {
loader: "obj-loader",
options: {
dummyText: "lorem ipsum"
}
}
}
]
}
};
/loaders/obj-loader.js
const loaderUtils = require("loader-utils"),
schemaUtils = require("schema-utils");
function loader(source) {
const options = loaderUtils.getOptions(this);
console.log(options);
let tmp = process.env.TEST_VAR;
if (tmp === undefined) {
console.warn("TEST_VAR is not defined");
}
return `export default function(){return "TEST_VAR: ${tmp} || ${source} || ${options.dummyText}";}`;
}
module.exports = loader;
/res/obj/dummy.obj
Hello
/src/index.js
import dummy from "../res/obj/dummy.obj";
(function () {
"use strict";
function main() {
document.querySelector("p").innerHTML = dummy();
}
document.addEventListener("DOMContentLoaded", main);
}());
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<title>Webpack Test</title>
<script src="dist/bundle.js"></script>
</head>
<bod>
<main>
<h1>Webpack Test</h1>
<p>Lorem Ipsum</p>
</main>
</bod>
</html>
Whenever I build and run this webpage, I get what amounts to:
<main>
<h1>Webpack Test</h1>
<p>TEST_VAR: fizzbuzz || Hello || lorem ipsum</p>
</main>
Where "fizzbuzz" was added from the environment variable, "Hello" the source is from /res/obj/dummy.obj, and "lorem ipsum" is the text that I specified as the dummyText option for the obj-loader. However, it could have just as easily come from an environment variable that I set.
Additional update based on edit
Considering your edit, what about this; instead of setting the rule in defaults set it in the exported function to get scope access. Then add your loader rules based on the new rule syntax: module.loaders is now module.rules. That way you have access to options in your custom loader and thus have access to BUILD_CONFIG.
Newer webpack.config.js
// Hypothetically you run:
// export BUILD_CONFIG=some-config.js
// Which sets:
// process.env.BUILD_CONFIG = "some-config.js";
// New Webpack loader/rules syntax
// https://webpack.js.org/guides/migrating/#module-loaders-is-now-module-rules
let defaults = {
module: {
rules: [
/*
*Set this later in exported function
{
test: ".some.special.stuff",
use: {
loader: "my-own-loader",
options: {
some: "options"
}
}
}
*/
]
}
};
module.exports = function (content) {
let BUILD_CONFIG = require(process.env.BUILD_CONFIG);
// Other code
let dd = defaults;
defaults.module.rules.push({
test: ".some.special.stuff",
use: {
loader: "my-own-loader",
options: {
stuffFromEnvConfig: BUILD_CONFIG
}
}
});
return dd;
};
my-own-loader.js
const loaderUtils = require("loader-utils"),
schemaUtils = require("schema-utils");
module.exports = function main(content) {
const options = loaderUtils.getOptions(this);
console.log(options);
let tmp;
if (options.stuffFromEnvConfig.useFirst) {
tmp = "Fizz";
} else {
tmp = "Buzz";
}
return `export default function(){return "TEST_VAR: ${tmp}";}`;
};

Related

Unable to acess kotlin js from javascript

I'm facing the issue where I'm not able to call any kotlin js function and getting 'something' is not defined.
I've tried compiling project with gradle but ended up following this tutorial and compiling with npm.
I attached my project here
EDIT: tested with maven and worked. However since maven is deprecated, I would like to use gradle or npm.
html code:
<body>
<script src="test.js"></script> //file generated in bin/bundle
<script>
(function() {
let a = new test.Test(); //test - module, Test - my class, error occurrs at this line
a.test(); //test - method in class Test
})()
</script>
</body>
however it always results in
Uncaught ReferenceError: test is not defined
package.json:
{
"name": "test",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "shx rm -rf bin && webpack && shx rm -rf bin/build/kotlin-test*",
"test": "mocha bin/test"
},
"author": "",
"license": "ISC",
"dependencies": {
"kotlin": "^1.3.70"
},
"devDependencies": {
"#jetbrains/kotlin-webpack-plugin": "^3.0.2",
"kotlin-test": "^1.3.70",
"mocha": "^7.1.0",
"shx": "^0.3.2",
"webpack": "^4.42.0",
"webpack-cli": "^3.3.11"
},
"description": ""
}
webpack.config.js:
const KotlinWebpackPlugin = require('#jetbrains/kotlin-webpack-plugin');
module.exports = {
entry: 'test', // This tells webpack where to begin for bundling
resolve: {
modules: ['bin/build', 'node_modules'], // Webpack will use this to look for anything that is required
},
output: {
path: __dirname + '/bin/bundle', // This is where the bundle will go
filename: 'test.js', // The bundle will be called vectron.js
},
plugins: [
//Step one - Create a test build
new KotlinWebpackPlugin({
src: __dirname, // Build Everything
output: 'bin/test', // Output to bin/test
moduleName: 'test', // Will create vectron.js
moduleKind: 'commonjs', // Create commonjs modules
librariesAutoLookup: true, // Uses node_modules for libraries
packagesContents: [require('./package.json')], // Points to package.json for dependencies
}),
// Step two - Create a production build
new KotlinWebpackPlugin({
src: __dirname + '/src', // Build only what is in src
output: 'bin/build', // Output to bin/build
moduleName: 'test', // Create a file called vectron.js
moduleKind: 'commonjs', // Create commonjs modules
metaInfo: true, // Include .meta.js files
sourceMaps: true, // Include Source mappings
librariesAutoLookup: true, // Uses node_modules for libraries
packagesContents: [require('./package.json')], // Points to package.json for dependencies
}),
],
};
my class:
class Test {
fun test() {
println("test")
}
}
EDIT: npm compilation of test.js:
(function (_, Kotlin) {
'use strict';
var println = Kotlin.kotlin.io.println_s8jyv4$;
var Kind_CLASS = Kotlin.Kind.CLASS;
function Test() {
}
Test.prototype.test = function () {
println('test');
};
Test.$metadata$ = {
kind: Kind_CLASS,
simpleName: 'Test',
interfaces: []
};
_.Test = Test;
Kotlin.defineModule('test', _);
return _;
}(module.exports, require('kotlin'))); //error: module is not defined
//# sourceMappingURL=test.js.map
maven kotlin plugin output of test.js:
if (typeof kotlin === 'undefined') {
throw new Error("Error loading module 'test'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'test'.");
}var test = function (_, Kotlin) {
'use strict';
var println = Kotlin.kotlin.io.println_s8jyv4$;
var Kind_CLASS = Kotlin.Kind.CLASS;
function Test() {
}
Test.prototype.test = function () {
println('test');
};
Test.$metadata$ = {
kind: Kind_CLASS,
simpleName: 'Test',
interfaces: []
};
_.Test = Test;
Kotlin.defineModule('test', _);
return _;
}(typeof test === 'undefined' ? {} : test, kotlin);
moduleKind was wrong in the build.gradle. Setting it to 'plain' fixed the issue.
Thank you

webpack, restrict what can be imported

Is there a way in webpack to restrict what files can be imported?
Say I want to be able to import files that are in the same directory, as well as the parent directory, but nothing above that parent directory? For example:
These work
import { blah } from "./script.js";
import { blah2 } from "./../gui/textbox.js";
import { blah3 } from "./../I/can/go/as/deep/down/as/I/want/here.js";
But this wouldn't work
import { passwords } from "./../../passwords.txt";
Because that would go up two (or x number of) directories, instead of just one.
You can create a loader to restrict webpack imports to specific files.
// file: webpack.config.js
const path = require('path');
module.exports = {
...
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: path.resolve('my-webpack-loader.js'),
options: {/* ... */}
}
]
}
]
}
};
Then throw if the resource file is outside ./src and ./node_modules directory or any directory of your choice.
// file: my-webpack-loader.js
const { getOptions } = require('loader-utils');
const validateOptions = require('schema-utils');
const path = require('path');
const schema = {
type: 'object',
properties: {
test: {
type: 'string'
}
}
};
function handler(source) {
const options = getOptions(this);
if(this.resourcePath.indexOf(path.resolve('./node_modules')) !== 0) {
if(this.resourcePath.indexOf(path.resolve('./src')) !== 0) {
throw `Reseource loading restricted for ${this.resourcePath}`;
}
}
validateOptions(schema, options, 'My Webpack Loader');
return source;
}
module.exports = handler;
For more info see writing a webpack loader.
react-dev-utils has a plugin for this.
This Webpack plugin ensures that relative imports from app's source
directories don't reach outside of it.
var path = require('path');
var ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
module.exports = {
// ...
resolve: {
// ...
plugins: [
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
// ...
],
// ...
},
// ...
};

Override what's bundled in webpack based on file extension

I'm trying to figure out whether or not webpack can do something like this. I have some code that I want to be bundled for a specific device. So I created a ViewFactories.ios.tsx and I also have ViewFactories.tsx. The problem i'm encountering is that the if I ignore the .ios.tsx in the loader test, it still gets bundled. I'm using the ignore-bundler plugin to ignore the .ios.tsx files, but the reference is just empty. ie:
/** still getting loaded here: **/
const ViewFactories_ios_1 = __importDefault(__webpack_require__(/*! ./ViewFactories.ios */ "./build/app/src/modules/layouts/factories/ViewFactories.ios.tsx"));
/*** the referenced section, but blank now ***/
/***/ "./build/app/src/modules/layouts/factories/ViewFactories.ios.tsx":
/*!***********************************************************************!*\
!*** ./build/app/src/modules/layouts/factories/ViewFactories.ios.tsx ***!
\***********************************************************************/
/*! dynamic exports provided */
/*! all exports used */
/***/ (function(module, exports) {
/***/ }),
What I really want is the reference to ViewFactories.tsx instead of ViewFactories.ios.tsx.
Is there some sort of dependency graph in webpack that I can access to tell the loader to use the default instead of the .ios.tsx?
My webpack config:
{
test: (modulePath) => {
if (/\.ios\./.test(modulePath)) {
console.log(modulePath);
return false;
}
return /\.tsx?$/.test(modulePath);
},
loader: "awesome-typescript-loader",
options: {
configFileName: 'tsconfig.web.json',
transpileOnly: true,
errorsAsWarnings: true,
}
},
{
test: (modulePath) => {
if (/\.ios\./.test(modulePath)) {
console.log('Ignored: ', modulePath);
return true;
}
return false;
},
loader: 'ignore-loader'
},
Just build two bundles, with shared config options for whatever is the same?
// webpack.config.js for two different bundles
let sharedModuleDef = {
rules: ...
}
let sharedPlugins = ...
let iosBundle = {
entry: "src/ios.entry.js",
output: {
path: ...
filename: "ios.bundle.js"
},
module: sharedModuleDef,
...
};
let everythingElse = {
entry: "src/main.entry.js",
output: {
path: ...
filename: "standard.bundle.js"
},
module: sharedModuleDef,
...
};
module.exports = [iosBundle, everythingElse];

Webpack: Trying to expose a bundled object to be usable by other scripts, object is still undefined

I'm trying to get just the basics down, transpiling a jsx file to js. However, my transpiled code needs to be called by non-transpiled code. output.library is supposed to help with that.
In the resulting bundle I see a definition for var react. But just after stepping through the entire bundle, it's clear react still isn't getting set.
my webpack.config.js
var webpack = require('webpack');
var path = require('path');
module.exports = {
entry: "./public/js/ui/react/dialog.jsx",
output: {
path: path.resolve(__dirname, "public/js/ui/react/"),
filename: "bundle.js",
libraryTarget: "var",
library: "react"
},
resolve: {
extensions: ['.js', '.jsx']
},
module: {
rules: [
{
test: /\.jsx$/,
loader: 'babel-loader',
exclude: [
path.resolve(__dirname, "node_modules/")
],
query: {
presets: ['es2015', "react"]
}
}
]
},
node: {
fs: "empty"
}
}
and the jsx I am trying to transpile:
'use strict';
react.Dialog = class extends React.Component {
render() {
return (
<div class="bubble-speech">Hello World</div>
)
}
}
elsewhere in my code, AND BEFORE THE BUNDLE, I have this, so that the react.Dialog assignment is not a null reference error:
var react = {};
If I take that one line away, the bundle.js will throw an error trying to assign react.Dialog. But if I leave it in, var react remains set to the empty object. That seems like a contradiction! What am I missing here?
I think react should be set as an externally defined global var, like this:
{
output: {
// export itself to a global var
libraryTarget: "var",
// name of the global var: "Foo"
library: "Foo"
},
externals: {
// require("react") is external and available
// on the global var React
"react": "React"
}
}

Exporting Typescript function in Webpack bundle built with gulp, empty object in browser

I'm trying to build a js bundle from typescript with exports that I can call directly from an inline script, but I don't seem to be able to get it to work.
I have a Typescript source file like this:
export namespace Init {
export function index() {
// do some things
}
}
export function blahblah() { console.log('blahblah') }
This sits alongside some other sources in the same folder src/ts/*.ts. My webpack.config.js looks like this:
'use strict';
module.exports = {
context: __dirname,
output: {
filename: 'bundle.js',
library: 'bitthicket',
},
devtool: 'sourcemap',
resolve: {
extensions: [ '.ts', '.tsx', '.js' ]
},
module: {
rules: [
{ test: /\.tsx?$/, exclude: /node_modules/, loader: 'ts-loader' }
]
}
}
And finally, my gulpfile.js looks like this:
var gulp = require('gulp')
var gutil = require('gulp-util')
var plumber = require('gulp-plumber')
var watch = require('gulp-watch')
var sass = require('gulp-sass')
var ts = require('gulp-typescript')
var clean = require('gulp-clean')
var webpack = require('webpack')
var webpack_config = require('./webpack.config.js')
var webpack_stream = require('webpack-stream')
let _sass = () =>
gulp.src('src/css/**/*.scss')
.pipe(plumber())
.pipe(sass())
.pipe(gulp.dest('static/css'))
.on('end', () => gutil.log('_sass: done'))
let _webpack = () =>
gulp.src('src/ts/**/*.ts')
.pipe(plumber())
.pipe(webpack_stream(webpack_config, webpack))
.pipe(gulp.dest('static/js'))
.on('end', () => gutil.log('_webpack: done'))
gulp.task('build:sass', _sass)
gulp.task('clean:sass', () => gulp.src('static/css/**/*').pipe(clean()))
gulp.task('watch:sass', () => watch('src/css/**/*.scss', () => _sass()))
gulp.task('build:webpack', _webpack)
gulp.task('clean:ts', () => gulp.src('static/js/**/*').pipe(clean()))
gulp.task('watch:webpack', () => watch('src/ts/**/*.ts', () => _webpack()))
gulp.task('watch', ['watch:sass', 'watch:webpack'])
gulp.task('clean', ['clean:sass', 'clean:ts'])
gulp.task('build', ['build:sass', 'build:webpack'])
gulp.task('default', ['clean', 'build'])
In an html file:
<script src="bundle.js" async></script>
<script>window.onload = function() { console.log(bitthicket); }</script>
What's happening, as I understand it, is that webpack is getting the piped list of sources as entry files, as though they were placed on the commandline like webpack {src list} bundle.js. But I've set my library output option, and the bitthicket variable does get set, but it's an empty object. What happened to Init.index?
Edit: I added the export keyword to the Init namespace, as well as a plain function to init.ts to see what would happen - but the resulting object still doesn't have those functions.
Here's the resulting webpack bootstrapper argument:
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
"use strict";
exports.__esModule = true;
var _ = __webpack_require__(2);
var Init;
(function (Init) {
function index() {
Nav.attachNavbarScrollWatcher();
document.addEventListener('scroll', function (event) {
var articles = document.querySelectorAll('.featured column.summary article');
var visibleArticle = _.find(articles, function (el) { return Util.isElementInViewport(el); });
console.log("{0} just became visible", visibleArticle);
});
}
Init.index = index;
})(Init = exports.Init || (exports.Init = {}));
function blahblah() {
console.log('blahblah');
}
exports.blahblah = blahblah;
/***/ }),
As you can see, exports.blahblah and exports.Init.index are assigned in this block - but the object I get back in my browser looks like this:
The real answer to my question is
Do you even module, bro?
I just flat-out misunderstood the relationship between what typescript calls namespaces and modules, and what webpack thinks modules are.
After this exercise in futility, I'm not even sure what the point of Typescript namespaces are anymore, unless you have a multi-stage build process where you use tsc and tsconfig.json to create multiple transpiled bundles that will later be re-bundled by some other loader like Webpack.
Anyway, to the point:
Typescript "namespaces" mean nothing at all to Webpack (more specifically, ts-loader: see TypeStrong/ts-loader#193). Webpack is not able to walk namespace dependencies, and so you wind up with undefined symbols in the bundle, which means the webpackBootstrap function also had no idea how to load things in the right order. The entry point in my bundle was kinda like this:
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
__webpack_require__(1); // <-- actual entry point, exports are lost
__webpack_require__(2);
exports = __webpack_require__(4); // <-- not entry point, nothing is exported here anyway
/***/ }),
...
Webpack (and ts-loader) require a form of modules that it understands in order to make the bootstrapper load things correctly.
I'm still using a Typescript namespace for structure, but I'm exporting it and using import to indicate dependencies on other Typescript modules:
import * as _ from 'lodash'
import * as Nav from './nav'
import * as Util from './util'
export namespace Init {
// ...
}
The main thing is the expression of the dependencies as es2015-style import/exports.

Categories

Resources