Angular 6 typescript integration with KotlinJs - javascript

I have just managed to import Kotlin compiled javascript module in an angular 6 typescript file. It was not easy and the result confuses me. I wanna know if more elegant way exists.
Originally I take a Kotlin file:
package com.example.test
data class SomeInterface(
var id: String? = null,
var value: String? = null
) {
}
It well compiles to the following JavaScript
(function (root, factory) {
if (typeof define === 'function' && define.amd)
define(['exports', 'kotlin'], factory);
else if (typeof exports === 'object')
factory(module.exports, require('kotlin'));
else {
if (typeof kotlin === 'undefined') {
throw new Error("Error loading module 'TestKotlinCompiled'. Its dependency 'kotlin' was not found. Please, check whether 'kotlin' is loaded prior to 'TestKotlinCompiled'.");
}
root.TestKotlinCompiled = factory(typeof TestKotlinCompiled === 'undefined' ? {} : TestKotlinCompiled, kotlin);
}
}(this, function (_, Kotlin) {
'use strict';
var Kind_CLASS = Kotlin.Kind.CLASS;
function SomeInterface(id, value) {
if (id === void 0)
id = null;
if (value === void 0)
value = null;
this.id = id;
this.value = value;
}
SomeInterface.$metadata$ = {
kind: Kind_CLASS,
simpleName: 'SomeInterface',
interfaces: []
};
SomeInterface.prototype.component1 = function () {
return this.id;
};
SomeInterface.prototype.component2 = function () {
return this.value;
};
SomeInterface.prototype.copy_rkkr90$ = function (id, value) {
return new SomeInterface(id === void 0 ? this.id : id, value === void 0 ? this.value : value);
};
SomeInterface.prototype.toString = function () {
return 'SomeInterface(id=' + Kotlin.toString(this.id) + (', value=' + Kotlin.toString(this.value)) + ')';
};
SomeInterface.prototype.hashCode = function () {
var result = 0;
result = result * 31 + Kotlin.hashCode(this.id) | 0;
result = result * 31 + Kotlin.hashCode(this.value) | 0;
return result;
};
SomeInterface.prototype.equals = function (other) {
return this === other || (other !== null && (typeof other === 'object' && (Object.getPrototypeOf(this) === Object.getPrototypeOf(other) && (Kotlin.equals(this.id, other.id) && Kotlin.equals(this.value, other.value)))));
};
var package$com = _.com || (_.com = {});
var package$example = package$com.example || (package$com.example = {});
var package$test = package$example.test || (package$example.test = {});
package$test.SomeInterface = SomeInterface;
Kotlin.defineModule('TestKotlinCompiled', _);
return _;
}));
In package.json I add "kotlin": "^1.2.70", to the dependencies section.
In angular component I have to use such a code for import.
import * as TestKotlinCompiled from "../../generated/TestKotlinCompiled";
// #ts-ignore
const SomeInterface = TestKotlinCompiled.com.example.test.SomeInterface;
// #ts-ignore
type SomeInterface = TestKotlinCompiled.com.example.test.SomeInterface;
This is minimal mandatory code to use class SomeInterfac in the package com.example.test generated to the module TestKotlinCompiled.
The problems here are following.
// #ts-ignore is required because at the compile time the ts-comiler does not see the content of the module being imported.
const is required for new SomeInterface()
type is required for let x: SomeInterface;
All these look terribly hacky.
I wold like something easier like
import {SomeInterface} from '../../generated/TestKotlinCompiled' using namespace com.example.test without const and type.
So, is there a way to simplify my above code?

I succeeded to improve little bit usability of KotlinJs in Angular. I dispose my experiments in https://github.com/svok/kotlin-multiplatform-sample
First, we must create a multiplatform submodule in Gradle. In that we generate js files (among other possible platforms).
Then we add to package.json...
{
"dependencies": {
"kotlin": "^1.3.21",
"proj-common": "file:../build/javascript-compiled"
}
}
proj-common is our compiled Kotlin module. The path there is where kotlin-js files are built to.
Thus, in typescript we just use one more npm module
import {sample} from 'proj-common/proj-common';
// For class Sample
sample = new sample.Sample();
// For object Platform
platform = sample.Platform;
Compilation goes well with no neсessity to use // #ts-ignore
Update
In the above explanation there was a problem with subdependencies. They were not exported, but not all subdependencies have their equivalents in npm repository. The below code solves this problem.
tasks {
task<Sync>("assembleWeb") {
val dependencies = configurations.get("jsMainImplementation").map {
val file = it
val (tDir, tVer) = "^(.*)-([\\d.]+-\\w+|[\\d.]+)\\.jar$"
.toRegex()
.find(file.name)
?.groupValues
?.drop(1)
?: listOf("", "")
var jsFile: File? = null
copy {
from(zipTree(file.absolutePath), {
includeEmptyDirs = false
include { fileTreeElement ->
val path = fileTreeElement.path
val res = (path.endsWith(".js") || path.endsWith(".map"))
&& (path.startsWith("META-INF/resources/") || !path.startsWith("META-INF/"))
if (res && path.endsWith(".js") && ! path.endsWith(".meta.js")) jsFile = fileTreeElement.file
res
}
})
into("$npmTarget/$tDir")
}
jsFile?.also { packageJson(tDir, it, tVer) }
tDir to jsFile
}
.filter { it.second != null }
.map { it.first to it.second!! }
.toMap()
packageJson(npmDir, File(jsOutputFile), project.version.toString(), dependencies)
dependsOn("jsMainClasses")
}
assemble.get().dependsOn("assembleWeb")
}
fun packageJson(dir: String, jsFile: File, version: String, dependencies: Map<String, File> = emptyMap()) {
val deps = dependencies.map {
""""${js2Name(it.value)}": "file:../${it.key}""""
}.joinToString(",\n ")
val text = """
{
"name": "${js2Name(jsFile)}",
"version": "${version}",
"main": "./${jsFile.name}",
"dependencies": {
${deps}
}
}
""".trimIndent()
File("$npmTarget/$dir/package.json").apply {
if (parentFile.exists()) {
parentFile.delete()
}
parentFile.mkdirs()
writeText(text)
}
}
fun js2Name(jsFile: File) = jsFile.name.replace("""\.js$""".toRegex(), "")
Then, import from the front submodule:
{
"dependencies": {
"proj-common": "file:../build/npm"
}
}
And in the typescript file:
import {sample} from 'proj-common';
// For class Sample
sample = new sample.Sample();
// For object Platform
platform = sample.Platform;
The sample project see at https://github.com/svok/kotlin-multiplatform-sample
Update 2
Now you can create full stack projects with kotlin common subproject as easy as just attaching a plugin in gradle
plugins {
id("com.crowdproj.plugins.jar2npm")
}
This plugin will automatically inject all your kotlin-js jar packages into your node_modules during compilation.
The https://github.com/svok/kotlin-multiplatform-sample project is now rewritten with this plugin. See proj-angularfront submodule.

I too had a go and solving this integration,
There are a number of issues to overcome, i.e.
generating typescript declaration files
unpacking kotlin JS modules into node_modules
third-party libraries
Blog post describing the issues is here https://medium.com/#dr.david.h.akehurst/building-applications-with-kotlin-and-typescript-8a165e76252c
I also created a gradle plugin that make it all alot easier,
https://github.com/dhakehurst/net.akehurst.kotlin.kt2ts

Related

Change syntax of JS file so it can be `required()` into another file with webpack

First time I am using webpack on Laravel 5.4 and playing around to learn.
I have this teal.js file which came from this 3d dice example.
"use strict";
window.teal = {};
window.$t = window.teal;
teal.copyto = function(obj, res) {
if (obj == null || typeof obj !== 'object') return obj;
if (obj instanceof Array) {
for (var i = obj.length - 1; i >= 0; --i)
res[i] = $t.copy(obj[i]);
}
else {
for (var i in obj) {
if (obj.hasOwnProperty(i))
res[i] = $t.copy(obj[i]);
}
}
return res;
};
teal.copy = function(obj) {
if (!obj) return obj;
return teal.copyto(obj, new obj.constructor());
};
teal.element = function(name, props, place) {
var dom = document.createElement(name);
if (props) for (var i in props) dom.setAttribute(i, props[i]);
if (place) place.appendChild(dom);
return dom;
};
.... MANY MORE
I want to be able to include it into my js bootstrap file.
window.teal = require(./dice/teal);
This is not working. First time with webpack coming from gulp where files are just concatenated, I am learning all about scopes and webpack.
What is missing in terms of syntax so I can require teal.js?
The js bootstrap code is:
window.teal = require('./dice/teal'); <-- problem
window.CANNON = require('./dice/cannon.min'); <-- ok
window.THREE = require('./dice/three.min'); <-- ok
window.dice = require('./dice/dice'); <-- ok
If you just want it to work, without it beeing refactored, you could try something dirty like this:
Below "use strict" remove:
window.teal = {};
window.$t = window.teal;
and add:
let teal = {}
Finally, at the end of the file add:
module.exports = window.teal
This is the fastest solution I could think of.
This way you get rid of the non existent window object, depending on your environment, and expose all the functions assigned to the teal object to your module exports.
Maybe I missed something about the structure of your file, tell me if its not working.

Turn multiple calls into one call chain

I'm making a library which exports one function (make) on global namespace (app) for defining a module or referencing it, similar to what angular.module does.
When I call it, I make sure to not store any reference to the make(name, [deps]) call so it will always use the main app object.
Usage is like this:
// Define 'hello'
app.make('hello', []);
// Define 'one' in 'hello'
app.make('hello').
data('one', 'thing');
// Multiple calls
app.make('hello').
data('two', 'thing').
attr('third', 'no').
data('four', 'empty');
And I want the above code turn into it:
app.make('hello', []).
data('one', 'thing').
data('two', 'thing').
attr('third', 'no').
data('four', 'empty');
So it should turn multiple separated calls to the return from make into just a big one (order doesn't matter and there are no side effects).
What I tried:
I'm planning to use esprima, estraverse and escodegen, here's what I actually have:
const fs = require('fs'),
esprima = require('esprima'),
estraverse = require('estraverse');
const modules = Object.create(null);
fs.readFile('sample.js', 'utf8', (err, data) => {
const tree = esprima.parse(data);
// Find module definitions
estraverse.traverse(tree, {
enter(node) {
const args = node.arguments;
if (isDefinitionCall(node) && args.length == 2) {
modules[args[0].value] = {
node,
childs: []
};
}
}
});
// Find module usages
estraverse.traverse(tree, {
enter(node) {
if (isGetterCall(node)) {
// What to store here?
// And how to modify the AST to turn
// everything into just chained call?
}
}
});
console.log('Modules found: ' + Object.keys(modules).join(', '));
});
function isDefinitionCall(node) {
return node.type == 'CallExpression' &&
node.callee &&
node.callee.object &&
node.callee.property &&
node.callee.type == 'MemberExpression' &&
node.callee.object.name == 'app' &&
node.callee.object.type == 'Identifier' &&
node.callee.property.type == 'Identifier' &&
node.callee.property.name == 'make';
}
function isGetterCall(node) {
return node.type == 'CallExpression' &&
node.callee &&
node.callee.object &&
isDefinitionCall(node.callee.object);
}
My question is: how can I move around the AST and get what I want done?
Thanks in advance!

Custom script in Meteor

I just started playing with Meteor and i was testing few things. Based on the Meteor Guide i was testing below code to understand the best way to write packages but i never been successful. What is wrong with the following code and what is good way (if not all but atleast few good ways) to write the packages for Meteor app which can be placed in lib folder.
/lib/exports.js
if (org === void 0){
var org = {}
}
if(bjse === void 0){
var bjse = {};
if(typeof exports != "undefined"){
bjse = exports;
}
bjse.api = {};
}
/lib/file1.js
// mypackage.js
bjse.api.Whirlygig = function (name) {
var self = this;
self.name = name; // name of the remote weasel
self.values = {}; // remote key name -> 0-indexed value
};
_.extend(Whirlygig.prototype, {
// Take a key/value pair from the remote Weasel and save it locally.
addValue: function (x) {
// Weasels use 1-indexed arrays. Subtract 1 to convert to 0-indexed.
self.values[x.key] = x.value - 1;
},
// Return a list of stored values in a format suitable for sending to
// a Weasel.
serialize: function () {
return _.map(self.values, function (v, k) {
var newVal = mungeValue(v, false /* foldValue */);
// Weasels use 1-indexed arrays. Add 1 to convert back to 1-indexed.
newVal = newVal + 1;
return {key: k, value: newVal};
});
}
});
/server/methods.js
Meteor.methods({
createConnections: function(){
....
var serializeObj = bjse.api.Whirlygig.serialize(..);
But i get Whirlygig not defined.
Update
I want to use bjse as namespace but it always appears as not defined in other files.
In exports you want:
if (typeof(org) === 'undefined'){
var org = {}
}
if(typeof(bjse) === 'undefined'){
var bjse = {};
if(typeof exports != "undefined"){
bjse = exports;
}
bjse.api = {};
}

Is this "DI" structure impossible, or should I look for a bug?

Explanation:
As a personal project, I'm trying to create my own lightweight version of Dependency Injection for JavaScript - Some would probably disagree with calling this DI because it has no interfaces, but I arrived at the conclusion that interfaces were overkill in JS since we can so easily type check. I have looked at the source of Angular, but I just feel like the complexity there may be overkill for my projects, and I'm interested in attempting my own for a learning experience anyway.
Question:
My question is, fundamentally, is the syntax I'm trying to implement impossible or not?
I'll explain my goal for the syntax, then provide the error and code snippet, and below that I'll post the full code.
Goal for Syntax:
I'd like the creation of a component, and injection of dependencies to work like this, where everything is a component, and anything can be a dependency. I created scope with a string path, using "/scopeName/subScopeName:componentName" to select a scope, so that code users can select the scope while defining the component in a simple way, using a ":" to select a component from the scope.
var JHTML = new Viziion('JHTML');
JHTML.addScope('/generate');
/* ...snip - see full code for the process component - snip ... */
JHTML.addComponent('/generate:init', function (jsonInput, process) {
var html = process(jsonInput);
return html;
}).inject([null, '/generate:process']);
The inject function just takes an array of component paths in the order the component's arguments are expected. null can be used to skip, allowing direct argument input instead, as shown above.
I also have something I call hooks, which are components stored in a certain place, and then there's a function returnUserHandle which will return an object consisting of just the hooks, so all of the functions are hidden in closures, and you can feed the code user just the usable methods, clean and easy, and can produce the final product as a library without the wiring, no need for my DI framework as a dependency. Hopefully that makes sense.
Error:
Right now, running the code (which is a very simple library to generate HTML by parsing a JSON structure) I get the error that process is undefined in the line var html = process(jsonInput);. I was having trouble understanding whether this is a fundamental design problem, or just a bug. Maybe this syntax is not possible, I'm hoping you can tell me.
Code:
Here's the code, and a link to the JS Bin.
/* Dependency Injection Framework - viziion.js */
function Viziion(appName) {
if (typeof appName == 'string') {
var that = this;
this.name = appName;
this.focus = null;
this.scope = {
'/': {
'subScopes': {},
'components': {}
}
};
this.hooks = {};
this.addScope = function(scopeName) {
if (typeof scopeName == 'string') {
var scopeArray = scopeName.split('/');
var scope = that.scope['/'];
for (var i = 0; i < scopeArray.length; i++) {
if (scopeArray[i] !== "") {
if (scope.subScopes[scopeArray[i]]) {
scope = scope.subScopes[scopeArray[i]];
} else {
scope.subScopes[scopeArray[i]] = {
'subScopes': {},
'components': {}
};
}
}
}
} else {
throw 'Scope path must be a string.';
}
return that;
};
this.addComponent = function(componentName, func) {
if (typeof componentName == 'string') {
var scopeArray = componentName.split(':');
if (scopeArray.length == 2) {
var scope = that.scope['/'];
var scopeName = scopeArray[1];
scopeArray = scopeArray[0].split('/');
for (var i = 0; i < scopeArray.length; i++) {
if (scopeArray[i] !== "") {
if ((i + 1) === scopeArray.length) {
scope.components[scopeName] = func;
that.focus = scope.components[scopeName];
} else if (scope.subScopes[scopeArray[i]]) {
scope = scope.subScopes[scopeArray[i]];
} else {
throw 'Scope path is invalid.';
}
}
}
} else {
throw 'Path does not include a component.';
}
} else {
throw 'Component path must be a string1.';
}
return that;
};
this.returnComponent = function(componentName, callback) {
if (typeof componentName == 'string') {
var scopeArray = componentName.split(':');
if (scopeArray.length == 2) {
var scope = that.scope['/'];
var scopeName = scopeArray[1];
scopeArray = scopeArray[0].split('/');
for (var i = 0; i < scopeArray.length; i++) {
if (scopeArray[i] !== "") {
if ((i + 1) === scopeArray.length) {
//console.log('yep1');
//console.log(scope.components[scopeName]);
callback(scope.components[scopeName]);
} else if (scope.subScopes[scopeArray[i]]) {
scope = scope.subScopes[scopeArray[i]];
} else {
throw 'Scope path is invalid.';
}
}
}
} else {
throw 'Path does not include a component.';
}
} else {
throw 'Component path must be a string2.';
}
};
this.addHook = function(hookName, func) {
if (typeof hookName == 'string') {
that.hooks[hookName] = func;
that.focus = that.hooks[hookName];
} else {
throw 'Hook name must be a string.';
}
return that;
};
this.inject = function(dependencyArray) {
if (dependencyArray) {
var args = [];
for (var i = 0; i < dependencyArray.length; i++) {
if (dependencyArray[i] !== null) {
that.returnComponent(dependencyArray[i], function(dependency) {
args.push(dependency);
});
}
}
console.log(that.focus);
that.focus.apply(null, args);
return that;
}
};
this.returnUserHandle = function() {
return that.hooks;
};
} else {
throw 'Viziion name must be a string.';
}
}
/* JSON HTML Generator - A Simple Library Using Viziion */
var JHTML = new Viziion('JHTML');
JHTML.addScope('/generate');
JHTML.addComponent('/generate:process', function(children) {
var html = [];
var loop = function() {
for (var i = 0; i < children.length; i++) {
if (children[i].tag) {
html.push('<' + tag + '>');
if (children[i].children) {
loop();
}
html.push('</' + tag + '>');
return html;
} else {
throw '[JHTML] Bad syntax: Tag type is not defined on node.';
}
}
};
}).inject();
JHTML.addComponent('/generate:init', function(jsonInput, process) {
console.log(process);
var html = process(jsonInput);
return html;
}).inject([null, '/generate:process']);
JHTML.addHook('generate', function(jsonInput, init) {
var html = init(jsonInput);
return html;
}).inject([null, '/generate:init']);
handle = JHTML.returnUserHandle();
/* HTML Generator Syntax - Client */
var htmlChunk = [{
tag: '!DOCTYPEHTML'
}, {
tag: 'html',
children: [{
tag: 'head',
children: []
}, {
tag: 'body',
children: []
}]
}];
console.log(handle.generate(htmlChunk));
is the syntax I'm trying to implement impossible or not?
It's absolutely possible, and I'm sure with a bit of bugfixing it'd work just fine.
What you're describing is essentially the same as Asynchronous Module Definition (AMD) which is used extensively for handling code dependencies.
Rather than continuing to pursue your own version of the same concept, I recommend that you give requirejs a try and follow the existing standards with your projects.

Trying to understand object and method creation in javascript

I'm trying to understand the different ways to create objects and methods in javascript. I've read a lot of articles, blogs and stackoverflow questions and I think I get the notion in general. But I've encountered a small javascript library (written in coffeescript) and the the way it creates objects and methods confused me a little.
I've included a snippet but if you want you can find the complete script at instafeed.js.
Code:
(function() {
var Instafeed, root;
Instafeed = (function() {
function Instafeed(params) {
var option, value;
this.options = {
target: 'instafeed',
get: 'popular',
resolution: 'thumbnail',
sortBy: 'most-recent',
links: true,
limit: 15,
mock: false
};
if (typeof params === 'object') {
for (option in params) {
value = params[option];
this.options[option] = value;
}
}
}
Instafeed.prototype.run = function() {
var header, instanceName, script;
if (typeof this.options.clientId !== 'string') {
if (typeof this.options.accessToken !== 'string') {
throw new Error("Missing clientId or accessToken.");
}
}
if (typeof this.options.accessToken !== 'string') {
if (typeof this.options.clientId !== 'string') {
throw new Error("Missing clientId or accessToken.");
}
}
if ((this.options.before != null) && typeof this.options.before === 'function') {
this.options.before.call(this);
}
if (typeof document !== "undefined" && document !== null) {
script = document.createElement('script');
script.id = 'instafeed-fetcher';
script.src = this._buildUrl();
header = document.getElementsByTagName('head');
header[0].appendChild(script);
instanceName = "instafeedCache" + this.unique;
window[instanceName] = new Instafeed(this.options);
window[instanceName].unique = this.unique;
}
return true;
}
...
return Instafeed;
})();
root = typeof exports !== "undefined" && exports !== null ? exports : window;
root.Instafeed = Instafeed;
}).call(this);
I'm having difficulties understanding the following:
Why did the author prefer to wrap everything with (function(){...}).call(this);? Maybe to avoid creating global variables?
What purpose does the .call(this) part at the very end of the script serve?
Why did the author create the root variable and what are the following lines for?
root = typeof exports !== "undefined" && exports !== null ? exports : window;
root.Instafeed = Instafeed;
Since this the preferred way to create objects and methods in coffeescript I suppose this is one of the better ways to do it. But its advantages over the following version escapes me:
function Instafeed(params) {
...
}
Instafeed.prototype.run = function() {
...
}
Yes; this makes all formerly top-level vars into local variables.
It makes this equal to the global object inside the function
It lets it work as a CommonJS module (for Node.js or Browserify)

Categories