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!
Related
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
I am developing a site using a third-party CMS and I have to include functions across various parts of the content depending on which page is being displayed. To reduce the amount of functions being called on each page load, I would like to loop through an array of functions to check if they exist before firing them.
This single function would then be called at body onload.
I have adapted code from Javascript Array of Functions and How to implement an array of functions in Javascript? as well as isFunction.
My understanding was that I could put the functions in an array without () and they would not be called but in my console in Chrome an Uncaught Reference error is generated on the line in the array where a function name is mentioned.
e.g. the jb_underimage_height function is not in the code on all pages, so this generates the error when it does not exist.
Here is the code so far:
function jb_onloads() {
var functionArray = [
jb_category_price_POA,
jb_highlight_tech_columns,
jb_underimage_height,
jb_moveGuestButton,
jb_loginCheck,
jb_moveRefineSpan,
jb_style_from_url,
materials_dropdown,
jb_remove_search_spaces,
jb_init_social,
checkCookies,
jb_category_change_class,
jb_move_basket_text,
jb_style_form,
jb_checkNotifyEnvelope
]; // end of functionArray
$.each(functionArray, function(key, value) {
if(typeof functionArray[key] !== 'undefined' && typeof functionArray[key] === "function") { functionArray[key](); }
});
} // end of jb_onloads
And this was my workaround when I had to this.
function a() { alert ("I am a") };
function b() { alert ("I am b") };
var arr = [
typeof a === "function" && a || 0,
typeof b === "function" && b || 0,
typeof c === "function" && c || 0
];
arr.forEach(function(func) {
if(typeof func === "function") {
func();
}
});
maybe we can do it as:
1 function defining:
if (typeof myFuncCollections == "undefined") // window.myFuncCollections
myFuncCollections = {};
myFuncCollections.func1 = function func1() {
console.log("func1");
};
//or
myFuncCollections['funcname'] = function funcname() {
console.log("funcname");
}
....
2 jb_onloads()
function jb_onloads() {
if (typeof myFuncCollections == "undefined")
myFuncCollections = {};
$.each(myFuncCollections, function(i) {
myFuncCollections[i]();
});
}
3 call jb_onloads() after including 1 and 2. And That do not require inlcuding 1-script before 2-script. Also, your can use any function in 1-script outside jb_onloads after including 1-script.
Since using Global value, please use special prefix for naming your "myFuncCollections"
You are trying to insert function references to the array. But if the function is not defined then that name does not exists and thus the error.
Add them as strings
function jb_onloads() {
var functionArray = [
'jb_category_price_POA',
'jb_highlight_tech_columns',
'jb_underimage_height',
'jb_moveGuestButton',
'jb_loginCheck',
'jb_moveRefineSpan',
'jb_style_from_url',
'materials_dropdown',
'jb_remove_search_spaces',
'jb_init_social',
'checkCookies',
'jb_category_change_class',
'jb_move_basket_text',
'jb_style_form',
'jb_checkNotifyEnvelope'
]; // end of functionArray
$.each(functionArray, function(index, functionName) {
// assuming functions are in the global scope
var func = window[ functionName ],
funcType = typeof func;
if (funcType === "function") {
func();
}
});
} // end of jb_onloads
I would like to create my modules in a way, that they can be used with and without requirejs (without require js they should just work normally, so I have to make sure they load correctly, like putting script tags in the right order).
So jQuery does it kindof like this:
// export module
if ( typeof define === "function" && define.amd ) {
define(["dep"], function(dep){
dep.fn.test = test;
return dep;
});
}
else{
dep.fn.test = test;
}
The actual module is defined as like so
var dep = function(...){...}
This definition and the export part is within an IIFE to keep everything in out of the global scope.
Generally it works well with one exception, the dependency is not available.
This problem can be solved by defining the function within the define part, but this would mean defining it twice, in the define part and below in the else part.
How can I get this to work but only define the module once?
I have "plugin-like" extensions to a core dep which should all be in separate files so the main dep must be passed as a depenency
This works fine btw. but it would mean I write the code for test twice.
(function(){
// export module
if ( typeof define === "function" && define.amd ) {
define(["dep"], function(dep){
dep.fn.test = function(){...ssomething using dep...};
return dep;
});
}
else{
dep.fn.test = unction(){...ssomething using dep...};
}
})
Okay, I try another example
animate/animate.js (this is my main file)
define(function(){
...
return animate;
});
animate/modules/easing.js (this is a module file)
(function(){
var ease = function(){
// using animate main function from animate.js
// animate is not available here
...
};
if ( typeof define === "function" && define.amd ) {
define(["animate/animate"], function(animate){
// animate is available here
...
animate.fn.ease = ease;
return animate;
});
}
else
{
// if no requirejs, animate is global
animate.fn.ease = ease;
}
});
I think you're just writing the define incorrectly and so it's not getting registered. This is what I use.
if (typeof define === "function" && define.amd) {
define("telegraph", [], function () { return telegraph; });
}
Put in context
(function(window) {
var telegraph = function() { };
telegraph.prototype.test = function() {
// do something
};
if (typeof define === "function" && define.amd) {
define("telegraph", [], function () { return telegraph; });
}
window.telegraph = telegraph;
})(window);
EDIT
Do the question is really how do you define test and make use of dep internally so that you don't have to supply it as a dependency and can define a named dep module. One solution is to register the second-level functions in the constructor and capture this as self (or another variable) to use within the function. The key thing here is that you use define to define the named module and by using the captured context in the constructor, you don't need to supply the parent object as a dependency. Example (with working fiddle at http://jsfiddle.net/YS8v6/):
(function(){
var dep = function() {
var self = this;
self.fn.test = function() {
self.foo();
};
};
dep.prototype.foo = function() {
alert('foo');
};
dep.prototype.fn = function() {
};
if ( typeof define === "function" && define.amd ) {
define('dep', [], function() { return dep; });
}
})();
The actual problem seems to be that define is not available within the IIFE but window.define is. So passing define as an argument to the IIFE solves the problem.
(function(define){
// export module
if ( typeof define === "function" && define.amd ) {
define(["dep"], function(dep){
dep.fn.test = function(){...ssomething using dep...};
return dep;
});
}
else{
dep.fn.test = unction(){...ssomething using dep...};
}
}(window.define))
Before it would check for define, not find it and immediately try to attache it to dep.fn.test without the requirejs define part.
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)
I'm having trouble figuring out how I can take a string of an object name and check if that object actually exists.
What I'm trying to accomplish is have an array the defines the required objects for a particular JavaScript "module" to work, for instance:
var requiredImports = ['MyApp.Object1', 'MyApp.Object2'];
Then using requiredImports, I want to loop over them and check if the are defined. Without using the above array, I can do the following which is what I'm trying to accomplish:
if (MyApp.Object1 == undefined) {
alert('Missing MyApp.Object1');
}
But using the above, I'd have to hard code this for every module rather than making a generic method that I can just pass it an array of strings and have it effectively do the same check for me.
I tried doing this by just passing it the objects themselves such as:
var requiredImports = [MyApp.Object1, MyApp.Object2];
But that throws a JavaScript error when those objects do not exist, which is what I'm trying to catch.
var MyApp = {
Object1: {}
};
function exists(varName, scope) {
var parent = scope || window;
try {
varName.split('.').forEach(function (name) {
if (parent[name] === undefined) {
throw 'undefined';
}
parent = parent[name];
});
}
catch (ex) {
return false;
}
return true;
}
console.log(
exists('MyApp.Object1'), // true
exists('MyApp.Object2'), // false
exists('window'), // true
exists('document'), // true
exists('window.document') // true
);
// or
console.log(
['MyApp.Object1', 'MyApp.Object2', 'window', 'document', 'window.document'].filter(function (varName) {
return !exists(varName);
})
);
// => ["MyApp.Object2"]
Note: that forEach is ES5 and as such not implemented in some browsers. But if you'd go with this solution, there is a nice polyfill here.
You can check for definedness with
if ( typeof window['MyApp'] === 'undefined' ||
typeof window['MyApp']['Object1'] === 'undefined' )
{
alert('Missing MyApp.Object1');
}
and so on.
Assuming MyApp.Object1 is a global scope, window is the parent object and since that is the top level object, you don't need to prefix your global vars with it. So window.MyApp.Object1 is the same as MyApp.Object1 (again, assuming this is within global scope).
Also, in javascript, MyApp['Object1'] is the same as MyApp.Object1. So if we apply this principle to the main window object, you can check for window['MyApp'] or window['MyApp']['Object1'] and the key here is that you can replace 'MyApp' and 'Object1' with a variable.
Example:
/* check if a variable/object exists in the global scope) */
function checkIfExists(someVar) {
if (typeof(window[someVar]) == 'undefined')
return true;
return false;
}
var foo = 'bar';
alert(checkIfExists('foo'));
You can evaluate your custom expression in JavaScript. Consider the code below:
var MyApp = {
Object1: "foo",
Object2: "bar"
};
var IsExists = function(varName) {
return new Function('return typeof(' + varName + ') === "undefined" ? false : true;')();
};
USAGE
var requiredImports = ['MyApp.Object1', 'MyApp.Object2'];
for (var i = 0; i < requiredImports.length; i++)
{
alert(requiredImports[i] + ": " + IsExists(requiredImports[i]))
}
You only get error for first level (MyApp in your example). I assume you have only a few first-level requires, so check them manually by window[x] which does not throw:
var requiredTopLevel = ['MyApp'];
for (var i = 0; i < requiredTopLevel.length; ++i) {
if ("undefined" === typeof window[requiredTopLevel[i]]) {
// problem with requiredTopLevel[i]
}
}
and then, to check nested requires (if top-level is present) you can use the values without fear. For example this will work:
var requiredNested = { 'Object1':MyApp.Object1, 'Object2':Myapp.Object2 };
for (var name in requiredNested) {
if ("undefined" === typeof requiredNested[name]) {
// problem with name
}
}