Webpack code splitting breaks instanceof between chunks - javascript

tl:dr;
class ModuleInBundleA extends ModuleInBundleC { … }
window.moduleInBundleB.foo(new ModuleInBundleA())
class ModuleInBundleB {
public foo(bar: ModuleInBundleA|ModuleInBundleC|number) {
if (bar instanceof ModuleInBundleA || bar instanceof ModuleInBundleC) {
// always false
…
}
}
}
Details:
I'm trying to start using TypeScript + Webpack 4.41.6 on the project that has mostly old codebase. Basically I want to package several small modules onto bundles to migrate softly without moving whole project onto new js stack.
I found out that Webpack can do this with code splitting, and package shared code into bundles on it's own with some configuration. However I can't really control what will be in every bundle unless I build every bundle separately and then only share types, using my own modules as external libraries and that's bit frustrating.
Maybe on this point you can say that I'm doing something wrong already and I would like to hear how can I achieve my goal of using bundles just as vanilla javascript (controlling defer/async on my own and using script tag on my own as well), and I don't really want to pack everything as an independent package with own configuration, types export and so on.
Hope you got overall context. Closer to the point.
I have the following function, that is bundled to it's own chunk called modal-manager.js.
public showModal (modal: ModalFilter|AbstractModal|number) {
let modalId: number;
console.log(modal);
console.log(typeof modal);
console.log(modal instanceof ModalFilter);
console.log(modal instanceof AbstractModal);
if (modal instanceof AbstractModal) {
modalId = modal.getId();
} else {
modalId = modal;
}
...
};
(Originally it had no ModalFilter as ModalFilter inherits AbstractModal but I included it for demonstration purposes)
The abstract modal is bundled automatically to modal-utils.js as it's shared between modules.
Next, I have another big bundle called filter.js. This one literally creates instance of ModalFilter const modalFilter = new ModalFilter(...). I think it's work mentioning that instance of modalFilter declared to the global window variable. The trouble is that filter.js calls modal.js code (through window.modalFilter.showModal(modalFilter)) with no problems whatsoever, but I see the following result of console.log:
ModalFilter {shown: false, loading: false, closing: false, html: init(1), id: 0, …}
modal.bundle.23e2a2cb.js:264 object
modal.bundle.23e2a2cb.js:265 false
modal.bundle.23e2a2cb.js:266 false
I disabled mapping to get more into code and see this:
ModalManager.prototype.showModal = function (modal) {
var modalId;
console.log(modal);
console.log(typeof modal);
console.log(modal instanceof _builder_component_modal_filter__WEBPACK_IMPORTED_MODULE_1__[/* default */ "a"]);
console.log(modal instanceof _modal_abstract__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"]);
if (modal instanceof _modal_abstract__WEBPACK_IMPORTED_MODULE_0__[/* default */ "a"]) {
modalId = modal.getId();
}
else {
modalId = modal;
}
this.modals[modalId].show();
this.scrollLock(modalId);
};
With my understanding of how javascript works, instanceof should check the object-creator function. As code chunks separated (modal.js has no same code with modal-utils.js) the creator function should be the same. However, getting more to the details I see that webpackJsonp can be really tricky and calling them from kind-of independent environments, still it should be the same environment where FilterModal, AbstractModal is called. The ModalManager could have own environment I believe. But code called is 100% the same. Could that webpackJsonp bundle-arrays be the source of the problem? If so, how can I avoid that and make modal.js bundle understand that both filter.js and others reference the same AbstractModal from modal-utils.js?
If I'm doing it wrong, is there a simple way to start bundling small and efficient scripts build with TypeScript and Webpack (or other tools)?
Also, I see the externals feature of Webpack, but haven't figured out how to use that in my case. In general, I'm ok with current set up except instanceof issue. The reason I want to avoid multiple builds is that I'll probably have dozens of smaller bundles that shared across different modules and having dozen of npm packages for each seems excessive.

Prefacing this with; I don't know the answer to the exact issue that you are facing in regards to the instanceOf part of your question. This is aimed at the "how did you do it" part.
Approx. 4 weeks ago we also changed from a .js to .ts implementation with about 1-2 hunderd .js files. Obviously we didn't want to migrate these all at once over to .ts as the effort was too high.
What we ended up doing was identifying .js scripts which needed to run on specific pages and added these into webpack as entry files. Then for all of the other suporting scripts, if we required their contents in our new .ts files, we actually created a large index/barrel file for them all, imported them in and then webpack will automatically include these in the correct scope alongside their respective .ts files.
What does this look like?
legacy.index.ts: For every single supporting .js file that we wanted to reference in any way in .ts.
var someFile_js = require("someFile.js");
export { someFile_js };
This then allowed us to import and use this in the .ts files:
import { someFile_js } from './legacy.index';
In reply to #tonix. To load a defined list:
webpack.config
const SITE_INDEX = require('./path-to-js-file/list.js')
module.exports = {
entry: SITE_INDEX
...
}
list.js
{
"filename1": "./some-path/filename1.js"
"filename2": "./some-path/filename2.ts"
}

Related

Looking for a way to merge multiple js scripts into one for a sandboxed eletron app

I am currently developing an electron app, using typescript.
As the renderer is sandboxed for security reasons, I need to pass a single .js file to the main Window.
However, due to the script being more than 2500 lines, it is currently split in multiple imported files, as would be a normal project.
How could I then merge the js files produced by typescript into a single complete file that could be passed to the electron Window ?
I tried Webpack, but I'm starting to think this is not the tool designed to do it.
what I am looking for:
entry.ts
import { Thing } from "./Thing"
const thing = new Thing();
thing.doSomething("hello world");
Thing.ts
export class Thing {
doSomething(text: string) {
// Stuff
}
}
that would be processed into a single file:
main.js
class Thing {
doSomething(text) {
// Stuff
}
}
const thing = new Thing();
thing.doSomething("hello world");
Thanks for the help !

How to convert Javascript exported class to Kotlin/JS?

I am new to JS and to Kotlin/JS. I have the following minimal working Javascript code for a Plugin for Obsidian from an example. It works as expected:
var obsidian = require('obsidian');
class SomePlugin extends obsidian.Plugin {
onload() {
new obsidian.Notice('This is a notice!');
}
}
module.exports = Plugin;
I was hoping to extend this plugin using Kotlin as I know the language, but I have some problems converting this to Kotlin/JS. My approach so far:
The runnable project can be found here on Github. Run gradle build to generate the build folder. It will fail in the browser step, but that step is not necessary. After the build the generated js file can be found in build\js\packages\main\kotlin\main.js.
main.kt
#JsExport
class SomePlugin: Plugin() {
override fun onload() {
Notice("This is a notice!")
}
}
#JsModule("obsidian")
#JsNonModule // required by the umd moduletype
external open class Component {
open fun onload()
}
#JsModule("obsidian")
#JsNonModule
external open class Plugin : Component {
}
#JsModule("obsidian")
#JsNonModule
external open class Notice(message: String, timeout: Number = definedExternally) {
open fun hide()
}
Edit: Thanks to the comment of #S.Janssen I switched the module type to umd
build.gradle.kts
plugins {
kotlin("js") version "1.5.20"
}
group = "de.example"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
implementation(npm("obsidian", "0.12.5", false))
}
kotlin {
js(IR) {
binaries.executable()
browser {
webpackTask {
output.libraryTarget = "umd"
}
}
}
}
tasks.withType<KotlinJsCompile>().configureEach {
kotlinOptions.moduleKind = "umd"
}
I don't actually need a result that can be run in the browser, but without the browser definition, it would not even generate a js file. With the browser part, an exception is thrown saying Can't resolve 'obsidian' in 'path\kotlin'. But at least a .js file is created under build/js/packages/test/kotlin/test.js. However the code is completely different from my expected code and also is not accepted by obsidian as a valid plugin code. I also tried some other gradle options. like "umd", "amd", "plain", legacy compiler instead of IR, nodejs instead of browser. But nothing creates a runnable js file. The error messages differ. With the legacy compiler it requires the kotlin.js file, that it cannot find even if I put it right next to it in the folder or copy the content into the script.
How do I get code functionally similar to the Javascript code posted above? I understand that it will have overhead, but the code currently generated does not even define or export my class by my understanding.
The error message that I get from obisidan debugger:
Plugin failure: obsidian-sample-plugin TypeError: Object prototype may only be an Object or null: undefined
The code generated:
(function (root, factory) {
if (typeof define === 'function' && define.amd)
define(['exports', 'obsidian', 'obsidian', 'obsidian'], factory);
else if (typeof exports === 'object')
factory(module.exports, require('obsidian'), require('obsidian'), require('obsidian'));
else {
if (typeof Component === 'undefined') {
throw new Error("Error loading module 'main'. Its dependency 'obsidian' was not found. Please, check whether 'obsidian' is loaded prior to 'main'.");
}if (typeof Plugin === 'undefined') {
throw new Error("Error loading module 'main'. Its dependency 'obsidian' was not found. Please, check whether 'obsidian' is loaded prior to 'main'.");
}if (typeof Notice === 'undefined') {
throw new Error("Error loading module 'main'. Its dependency 'obsidian' was not found. Please, check whether 'obsidian' is loaded prior to 'main'.");
}root.main = factory(typeof main === 'undefined' ? {} : main, Component, Plugin, Notice);
}
}(this, function (_, Component, Plugin, Notice) {
'use strict';
SomePlugin.prototype = Object.create(Plugin.prototype);
SomePlugin.prototype.constructor = SomePlugin;
function Unit() {
Unit_instance = this;
}
Unit.$metadata$ = {
simpleName: 'Unit',
kind: 'object',
interfaces: []
};
var Unit_instance;
function Unit_getInstance() {
if (Unit_instance == null)
new Unit();
return Unit_instance;
}
function SomePlugin() {
Plugin.call(this);
}
SomePlugin.prototype.onload_sv8swh_k$ = function () {
new Notice('This is a notice!');
Unit_getInstance();
};
SomePlugin.prototype.onload = function () {
return this.onload_sv8swh_k$();
};
SomePlugin.$metadata$ = {
simpleName: 'SomePlugin',
kind: 'class',
interfaces: []
};
_.SomePlugin = SomePlugin;
return _;
}));
You can find a working example of what you're going for here. I'll go through some of the changes that needed to be made to your code one-by-one in this reply.
Being unable to resolve obsidian
Can't resolve 'obsidian' in 'path\kotlin' occurs because the obsidian-api package is not a standalone library. Instead, it only consist of a obsidian.d.ts file, which is a TypeScript declaration file. Similar to a header file in other languages, this header file does not provide any implementations, but only the signatures and types for the library – meaning Kotlin/JS' webpack (or any JavaScript tooling, for that matter) won't be able to resolve the actual implementations. This is expected, and can be addressed by declaring the module as external. To do so in Kotlin/JS, create a directory called webpack.config.d, and add a file 01.externals.js with the following content:
config.externals = {
obsidian: 'obsidian',
};
(You can actually find an equivalent snippet in the offical sample-plugin configuration, as well, since this isn't a Kotlin/JS specific problem)
Grouping multiple #JsModule declarations
Because you're importing multiple declarations from the same package, instead of annotating multiple signatures with #JsModule / #JsNonModule, you'll have to create a separate file, and annotate it with #file:#JsModule("...") / #file:JsNonModule:
#file:JsModule("obsidian")
#file:JsNonModule
open external class Component {
open fun onload()
open fun onunload()
}
open external class Plugin(
app: Any,
manifest: Any
) : Component
open external class Notice(message: String, timeout: Number = definedExternally) {
open fun hide()
}
Kotlin's ES5 vs Obsidian's ES6
Additionally, some of your problems stem from the fact that Obsidian's examples implicitly make the assumption that you are targeting ES6 (while Kotlin's current target is ES5). Specifically, this makes a difference in regards to how your plugin exports its members, as well as how classes are instantiated.
Inheritance
In regards to inheritance (since YourPlugin inherits from Plugin), ES6 classes automatically initialize the parent class with all arguments. This is something that is not supported in ES5's prototype inheritance. This is why in the snippet above, we need to explicitly pass the Plugin class constructor the app and manifest parameters, and pass them through in the implementation of your specific plugin:
class SomePlugin(
app: Any,
manifest: Any
) : Plugin(
app,
manifest
)
Exports / Module System
In regards to exporting your plugin, Obsidian expects either module.exports or exports.default to be your Plugin class directly. To achieve this exact export behavior, a few conditions need to be met, which is unfortunately a bit cumbersome:
- The library target needs to be CommonJS: output.libraryTarget = "commonjs" (not CommonJS2)
- To prevent creating a level of indirection, as is usually the case, the exported library need to be set to null: output.library = null
- To export your Plugin under as default, its class declaration needs to be marked as #JsName("default").

TypeScript Augmenting Window not working in separate files

I'm looking to add another property to window. I can do that with this:
// global.d.ts
import { IConfig } from './src/models';
export {};
declare global {
interface Window {
_env: IConfig;
}
}
But then when I try to reference this new property in a different file, it complains:
// src/util.ts
// Property '_env' does not exist on type 'Window & typeof globalThis'.
export const URL = `https://example.com/${window._env.path}`;
But when I combine these into the same file, everything is fine and there are no errors. Is there anyway I can have these in a separate file?
I'm using TypeScript 4.1.2.
I went down a bit of a rabbit hole but found this relevant documentation
I was able to accomplish this exactly as you have written (different type of course) in an existing angular 8 project and svelte project using their existing polyfills.ts files for my global declaration.
Are you sure you tsconfig.json is compiling everything correctly?

How to import/load entire file to include in my bundle?

How to include an entire file into my bundle main.js?
ES6 can import/export functions and classes. But what if i want to include the whole content from another file into my bundle main.js? how to do it?
I came across the query on Stackoverflow: Managing jQuery plugin dependency in webpack.
I'm not sure about this question though. Those options given there seem to target injecting implicit globals, configuring this, disabling AMD, to include large dists. I don't think this is what i want.
Let's say i have two files in src directory
1- rough.js
const rgh = "qwerty"
2- index.js
import './rough.js' //something like this
console.log (rgh)
Now what i expect in bundle.js is
const rgh = "query";
console.log(rgh)
I just want all the content inside one of my file to get all transported to index.js for webpack to bundle them
Those options given there seem to target injecting implicit globals,
configuring this, disabling AMD, to include large dists. I don't think
this is what i want.
To understand this you need to understand what webpack is doing for you. Web pack takes a series of Javascript files (and more importantly their contents) and parses these into one file. That's what it does from a file point of view, but if you ignore the file and think about what it does from a code point of view, it takes each one of the imported objects and makes them available to other objects depending upon the rules you define in your code (using import and export). You can think of this from a closure point of view something like this:
if you have some code like:
import a from 'a.js';
export default b(){
console.log(a.test());
}
This will be turned into something like, in one js file:
var a = (function() {
var testStr = "test";
function test(){
return testStr;
}
return {test:test};
})();
var b = (function(a) {
console.log(a.test());
})(a);
So you can see that the file isn't really important. What's important is the scope. b can use a because it is injected into it's scope (In this instance as a IIFE).
In the above example a and b are in the global scope but testStr isn't.
So when your talking about "importing my file", you need to forget about that and think about what objects in that file you want to import how. Any variables "in that file" declared directly var a = ....; are in the global scope. So it sounds like what you want to do is import the objects in that file into the global scope.
you just need to import that file in main.js
like this way

Creating a reusable RequireJs library

Given the following simplified scenario, how could I best construct my reusable component so that it is correctly consumable by another application, such that foo.js prints 23?
Reusable Component:
/home.js
/main.js
/stuff
foo.js
--
/home.js:
define([], function () { return 23; }
/stuff/foo.js:
define(['home'], function (home) { console.log(home); } // prints 23
Consumer Application:
/app.js
/main.js
/home.js
/views
template.html
/bower_components
/myReusableComponent
/home.js
/main.js
/stuff
foo.js
--
/home.js:
define([], function () { return 17; }
/bower_components/myReusableComponent/home.js:
define([], function () { return 23; }
/bower_components/myReusableComponent/stuff/foo.js:
define(['home'], function (home) { console.log(home); } // now prints 17
Is there a consumer application requirejs config that sets the baseUrl of any module in /myReusableComponent to '/myReusableComponent'? My reusable component should not have/need access to the root level of the consumer application anyway.
I have looked into the r.js optimizer, but it just outputs a bunch of define('stuff/foo', [], function ())... what happens if the consumer application has a stuff/foo.js too?
The only solution I have found so far is forcing the use of relative paths in all of my modules: define(['../home'], function (home) { console.log(home); } but I am hoping there is a more elegant way to solve this problem.
All feedback is very appreciated!
If you want to produce a library that is going to be usable in different applications, then you should use use relative paths when one module of your library refers to another, because this makes it more likely that whoever uses your library will be able to do so without having to do modify their RequireJS configuration.
Some clashes can be eliminated by the judicious use of map or paths. Ultimately, however, there are cases that cannot be solved (or at least not be solved as the user wants it) without having access to the unoptimized modules so you should distribute your library as an optimized bundle and provide the possibility to load it as a collection of unoptimized modules.

Categories

Resources