JavaScript Support: Difference between revisions

From Kolmafia
Jump to navigation Jump to search
Ikzann (talk | contribs)
m add revision
→‎Library Functions: neat trick for javascript scripts
 
(29 intermediate revisions by 3 users not shown)
Line 1: Line 1:
As of revision 20509, KoLmafia supports scripting in JavaScript! You can run JS code from the CLI using <code>js <nowiki><code></nowiki></code>, and you can call scripts through any of the normal methods. Consult and "lifecycle" scripts (e.g. <code>betweenBattleScript</code>) are supported as well as of revision 20519.
As of revision 20509, KoLmafia supports scripting in JavaScript! You can run JS code from the CLI using <code>js <nowiki><code></nowiki></code>, and you can call scripts through any of the normal methods. Consult and "lifecycle" scripts (e.g. <code>betweenBattleScript</code>) are supported, too! '''This support is still experimental - you have been warned.'''


* All the methods in the ASH runtime library are available on the <code>Lib</code> global object in JS, with names of methods converted to camelCase. So, for example, <code>print_html</code> in ASH becomes <code>Lib.printHtml</code> in JS.
== Basics ==
 
=== Library Functions ===
All functions in the ASH runtime library are available inside the built-in <code>kolmafia</code> module. To use these functions, you must use the <code>require()</code> function to import them:
 
<syntaxhighlight lang="js">
const { print, myName() } = require("kolmafia");
print("Hello, " + myName());
 
// Alternative
const kolmafia = require("kolmafia");
kolmafia.print("Hello, " + kolmafia.myName());
 
// Alternative to have all functions in the global namespace
Object.assign(globalThis, require("kolmafia"));
print("Hello, " + myName());
</syntaxhighlight>
 
The <code>require()</code> function is usually called once at the top of the script.
 
Names of ASH functions are converted to [[wikipedia:CamelCase|camelCase]]. For example, {{f|print_html}} in ASH becomes <code>printHtml()</code> in JavaScript:
 
{| class="wikitable"
! style="width: 50%" | ASH
! style="width: 50%" | JavaScript
|- style="vertical-align: top"
|
<syntaxhighlight lang="d">
print_html("<b>Some text</b>");
</syntaxhighlight>
|
<syntaxhighlight lang="js">
const { printHtml } = require("kolmafia");
print("<b>Some text</b>");
</syntaxhighlight>
|}
 
When directly executing inline JavaScript code with the <code>js</code> or <code>jsq</code> commands, all library functions are already imported for you, so you don't have to import them:
 
  > js print("My name is " + myName())
My name is <playername>
 
=== Importing and Exporting ===
KoLmafia uses the [[wikipedia:CommonJS|CommonJS]] module system, similar to [[wikipedia:Node.js|Node.js]]. You can export functions and values from one JavaScript file and import them from another JavaScript file:
 
{| class="wikitable"
! style="width: 50%" | <code>source.js</code>
! style="width: 50%" | <code>runner.js</code>
|- style="vertical-align: top"
|
<syntaxhighlight lang="js">
const { print } = require("kolmafia");
module.exports.hello = function hello() {
  print("Hello, world!");
};
</syntaxhighlight>
|
<syntaxhighlight lang="js">
const { hello } = require("./source.js");
hello();
</syntaxhighlight>
|}
 
Check out [https://flaviocopes.com/commonjs/ this guide] for more examples.
 
You can export a function named <code>main()</code> to run it only when the script is invoked directly. When your script is imported by another script, the <code>main()</code> function will be ignored.
 
You can also import ASH scripts from JavaScript code. For details, see [[JavaScript Support#ASH_and_JavaScript_Interoperability|ASH and JavaScript Interoperability]].
 
=== Data Type Classes ===
All [[Data Types#Special Datatypes|enumerated data types]] in ASH are available as classes in JavaScript. For example, {{type|monster}} is available as the <code>Monster</code> class, and {{type|item}} is available as the <code>Item</code> class.
 
Each enumerated class has the following static methods:
 
* <code><ClassName>.get()</code> takes a number or string and returns an object of the class.
** For example, <code>Monster.get("fiendish can of asparagus")</code> is equivalent to <code>$monster[ fiendish can of asparagus ]</code> in ASH. <code>Item.get(1)</code> is equivalent to <code>$item[ 1 ]</code>.
** This also accepts string representations of integers. For example, <code>Item.get(5)</code> and <code>Item.get("5")</code> return the same result.
* <code><ClassName>.get()</code> can also take an array of numbers and strings, and return an array of objects.
** For example, <code>Item.get(["seal-clubbing club", "pail", 5])</code> is similar to <code>$items[ seal-clubbing club, pail, 5 ]</code> in ASH. However, the JavaScript version returns an array of objects, instead of a boolean map.
** Passing an empty array returns an empty array (i.e. <code>Item.get([])</code> is ''not'' the same as <code>$items[]</code>).
* <code><ClassName>.all()</code> takes no argument and returns all possible values of the class.
** For example, <code>Monster.all()</code> returns an array of all known monsters. This is similar to <code>$monsters[]</code> in ASH.
 
Enumerated objects support the <code>toString()</code> method, which acts like the {{f|to_string}} ASH function.
 
To retrieve the numeric ID of enumerated objects, use the {{f|to_int|toInt}} library function:
 
<syntaxhighlight lang="js">
let item = Item.get("filthy lucre");
let itemId = toInt(item);
</syntaxhighlight>
 
=== Other ===
* ASH maps are converted to plain JS objects, and ASH arrays are converted to JS arrays.
* ASH maps are converted to plain JS objects, and ASH arrays are converted to JS arrays.
* You can look at the type reference for the JS <code>Lib</code> with <code>jsref</code>, which works just like <code>ashref</code>.
* You can look at the type reference for the JS version of the ASH runtime library with <code>jsref</code>, which works just like <code>ashref</code>.
* Objects like monsters and items can be constructed via the <code>Monster</code> and <code>Item</code> global objects, along with the rest of ASH's enumerated types (stat, phylum, location, etc.). <code>Monster.get</code> takes a number or a string, just like <code>$monster</code> in ASH, or an array of numbers and strings to construct an array of monsters. <code>Monster.all</code> works like <code>$monsters[]</code> in ASH; it takes no arguments and returns an array of all monsters.
 
* Mafia supports <code>require</code> for both ASH and JS scripts. For ASH scripts, it will execute top-level code but only export functions, not variables, in the top scope.
== ASH and JavaScript Interoperability ==
 
JavaScript scripts can <code>require()</code> ASH scripts and use their functions. When an ASH script is <code>require()</code>-ed by JavaScript code, KoLmafia will execute top-level code, but only export functions in the top-level scope. ASH variables are not exported.
 
For example, you can use [[Zlib]] if it is installed:
 
<syntaxhighlight lang="js" line>
const { getvar } = require("zlib.ash");
let myvar = getvar("SOME_VAR_NAME");
</syntaxhighlight>
 
ASH scripts cannot <code>import</code> JavaScript scripts.
 
== JavaScript Version and Features ==
KoLmafia uses the [[wikipedia:Rhino (JavaScript engine)|Rhino]] engine to execute JavaScript code. Rhino supports an older version of JavaScript called "ES5", plus some features from newer versions. This means that many JavaScript features that work in web browsers might not work in KoLmafia.
 
Here is an incomplete list of post-ES5 features supported by Rhino ([https://kolmafia.us/threads/javascript-bugs.25638/post-160384 source]):
 
=== Supported ===
* Syntax
** <code>let</code> and (partially) <code>const</code>
*** Does not support block-level scoping or temporal dead zones, meaning that you cannot use <code>const</code> for loop variables. <code>for (const a in obj) { ... }</code> is a syntax error.
** Array/object destructuring (but spread/rest syntax (<code>...</code>) is ''not'' supported)
** <code>for...of</code> loop
** Arrow functions: <code>() => {}</code>
** Octal and binary literals
* Features
** Symbol
** Set, Map, WeakSet, WeakMap
** ES2015 methods in Array, Math, Number, Object, String
** <code>Array.prototype.includes()</code>
** <code>String.prototype.padStart()/padEnd()/trimStart()/trimEnd()</code>
** TypedArray: Can be constructed, but most TypedArray-specific methods are unavailable.
 
=== Unsupported ===
* Syntax
** Spread/rest syntax (<code>...</code>)
** Object destructuring in assignments (variable declarations are OK)
** Template string literals: Backtick string literals (<code>``</code>) are not a syntax error, but are treated as plain string literals.
** Classes
** ECMAScript modules (<code>import</code>/<code>export</code>)
** Default function parameters
** Computed property names
** Shorthand property names (in object literals)
** Exponentiation operator (<code>**</code>)
** Async/Await
** Trailing commas in function definitions (oddly, trailing commas are supported in function ''calls'')
* Features
** Promise
** Proxy
** Reflect
 
=== Other ===
Most JavaScript globals available in browsers and/or server-side environments like Node.js are ''not'' available. This includes <code>alert()</code>, <code>console.log()</code>, and <code>setTimeout()</code>.
 
== Code Checking ==
 
You can set up [https://eslint.org/ ESLint] to check your code for errors.
 
The following ESLint configuration (<code>.eslintrc.json</code>) checks if your JavaScript code uses features unsupported by KoLmafia, and prevents ESLint from complaining about KoLmafia builtins.
 
<syntaxhighlight lang="json">
{
  "env": {
    "commonjs": true,
    "es6": true
  },
  "globals": {
    "Promise": "off",
    "Proxy": "off",
    "Reflect": "off",
    "Bounty": "readonly",
    "Class": "readonly",
    "Coinmaster": "readonly",
    "Effect": "readonly",
    "Element": "readonly",
    "Familiar": "readonly",
    "Item": "readonly",
    "Location": "readonly",
    "Monster": "readonly",
    "Phylum": "readonly",
    "Servant": "readonly",
    "Skill": "readonly",
    "Slot": "readonly",
    "Stat": "readonly",
    "Thrall": "readonly"
  },
  "parserOptions": {
    "ecmaVersion": 6
  },
  "rules": {
    "no-restricted-syntax": [
      "error",
      {
        "selector": "AssignmentExpression > ObjectPattern",
        "message": "Rhino does not support object destructuring in assignments (variable declarations are OK)"
      },
      {
        "selector": "AssignmentPattern",
        "message": "Rhino does not support default values for function parameters and array/object destructuring"
      },
      {
        "selector": "ClassDeclaration, ClassExpression",
        "message": "Rhino does not support ES2015 classes"
      },
      {
        "selector": ":matches(ForInStatement, ForOfStatement, ForStatement) > VariableDeclaration[kind=const]",
        "message": "Rhino does not support const declarations in the head of for-loops"
      },
      {
        "selector": "ObjectExpression > Property[shorthand=true]",
        "message": "Rhino does not support shorthand object properties"
      },
      {
        "selector": "Property[computed=true]",
        "message": "Rhino does not support computed object properties"
      },
      {
        "selector": "RestElement, SpreadElement",
        "message": "Rhino does not support spread/rest syntax"
      },
      {
        "selector": "TemplateLiteral",
        "message": "Rhino does not support template string literals"
      }
    ]
  }
}
</syntaxhighlight>
 
Note that this is unnecessary if you use a transpiler to convert modern JavaScript code to Rhino-compatible syntax.
 
== Transpiling ==
 
Various tools as Babel, Webpack, and TypeScript can convert modern JavaScript code (or another programming language) to legacy syntax supported by Rhino. These tools are called "transpilers". These tools allow you to enjoy the convenience and safety of modern language features, while still generating code that runs on KoLmafia.
 
=== [https://babeljs.io/ Babel] ===
 
Babel 7.15.0 supports Rhino as a compilation target.
 
The following config file (<code>babel.config.json</code>) will convert modern JavaScript syntax to Rhino-compatible ES5+:
 
<syntaxhighlight lang="json">
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false,
        "targets": {
          "rhino": "1.7.13"
        }
      }
    ]
  ]
}
</syntaxhighlight>
 
* Babel: As of r20558, you will still need to apply several patches to Babel in order to get babel-preset-env to work. See [https://github.com/phulin/bean-casual/tree/ts] for an example of a working Babel/Webpack/Typescript configuration; you'll need the configuration files and also the patches, which can be applied with patch-package.
 
=== [https://www.typescriptlang.org/ TypeScript] ===
 
TypeScript is a statically typed programming language that can be transpiled to JavaScript.
 
If you use TypeScript without Babel, you can set the <code>[https://www.typescriptlang.org/tsconfig#target target]</code> to <kbd>"ES5"</kbd>.
 
TypeScript does not provide [[wikipedia:Polyfill (programming)|polyfills]] for modern JavaScript APIs missing in Rhino. To use them, you must supply your own.
 
To avoid accidentally using any missing APIs, we recommend using the following configuration for your <code>tsconfig.json</code>:
 
<syntaxhighlight lang="js">
{
  "compilerOptions": {
    // JavaScript APIs supported by Rhino 1.7.13
    // See https://mozilla.github.io/rhino/compat/engines.html for more info
    "lib": [
      "ES5",
      "ES2015.Collection",
      "ES2015.Core",
      "ES2015.Generator",
      "ES2015.Iterable",
      "ES2015.Symbol",
      "ES2015.Symbol.WellKnown",
      "ES2016.Array.Include",
      "ES2017.String",
      "ES2019.String"
    ],
    // Rhino uses require() instead of import/export
    // Note: If you use Webpack or Rollup, change this to "ES2015"
    "module": "CommonJS",
    // Rhino supports ES5+
    // Note: If you use Babel with TypeScript, change this to "ESNext"
    "target": "ES5"
  }
}
</syntaxhighlight>
 
If you use a bundler such as [https://webpack.js.org/ Webpack] or [https://rollupjs.org/ Rollup], you should change the <code>module</code> to <kbd>"ES2015"</kbd>, and let the bundlers convert your code to CommonJS.
 
== History ==
* [https://kolmafia.us/threads/20520-this-is-a-big-patch-to-set-up-consult-scripts-lifecycle-scripts-choiceadventurescript.25660/ r20520]: Lifecycle scripts now support JavaScript.
* [https://kolmafia.us/threads/20620-remove-valueof-as-per-philmasterplus.25845/ r20620]: Enumerated objects no longer provide a custom <code>valueOf()</code> method. Use {{f|to_int|toInt}} instead.
* [https://kolmafia.us/threads/20780-javascript-runtime-no-longer-interrupts-when-library-functions-return-false.26174/ r20780]: When calling ASH runtime library functions from JavaScript, functions that return a {{type|boolean}} will return <code>false</code> on failure instead of throwing a string as an exception.

Latest revision as of 08:48, 13 December 2022

As of revision 20509, KoLmafia supports scripting in JavaScript! You can run JS code from the CLI using js <code>, and you can call scripts through any of the normal methods. Consult and "lifecycle" scripts (e.g. betweenBattleScript) are supported, too! This support is still experimental - you have been warned.

Basics

Library Functions

All functions in the ASH runtime library are available inside the built-in kolmafia module. To use these functions, you must use the require() function to import them:

const { print, myName() } = require("kolmafia");
print("Hello, " + myName());

// Alternative
const kolmafia = require("kolmafia");
kolmafia.print("Hello, " + kolmafia.myName());

// Alternative to have all functions in the global namespace
Object.assign(globalThis, require("kolmafia"));
print("Hello, " + myName());

The require() function is usually called once at the top of the script.

Names of ASH functions are converted to camelCase. For example, print_html() in ASH becomes printHtml() in JavaScript:

ASH JavaScript
print_html("<b>Some text</b>");
const { printHtml } = require("kolmafia");
print("<b>Some text</b>");

When directly executing inline JavaScript code with the js or jsq commands, all library functions are already imported for you, so you don't have to import them:

 > js print("My name is " + myName())
My name is <playername>

Importing and Exporting

KoLmafia uses the CommonJS module system, similar to Node.js. You can export functions and values from one JavaScript file and import them from another JavaScript file:

source.js runner.js
const { print } = require("kolmafia");
module.exports.hello = function hello() {
  print("Hello, world!");
};
const { hello } = require("./source.js");
hello();

Check out this guide for more examples.

You can export a function named main() to run it only when the script is invoked directly. When your script is imported by another script, the main() function will be ignored.

You can also import ASH scripts from JavaScript code. For details, see ASH and JavaScript Interoperability.

Data Type Classes

All enumerated data types in ASH are available as classes in JavaScript. For example, monster is available as the Monster class, and item is available as the Item class.

Each enumerated class has the following static methods:

  • <ClassName>.get() takes a number or string and returns an object of the class.
    • For example, Monster.get("fiendish can of asparagus") is equivalent to $monster[ fiendish can of asparagus ] in ASH. Item.get(1) is equivalent to $item[ 1 ].
    • This also accepts string representations of integers. For example, Item.get(5) and Item.get("5") return the same result.
  • <ClassName>.get() can also take an array of numbers and strings, and return an array of objects.
    • For example, Item.get(["seal-clubbing club", "pail", 5]) is similar to $items[ seal-clubbing club, pail, 5 ] in ASH. However, the JavaScript version returns an array of objects, instead of a boolean map.
    • Passing an empty array returns an empty array (i.e. Item.get([]) is not the same as $items[]).
  • <ClassName>.all() takes no argument and returns all possible values of the class.
    • For example, Monster.all() returns an array of all known monsters. This is similar to $monsters[] in ASH.

Enumerated objects support the toString() method, which acts like the to_string() ASH function.

To retrieve the numeric ID of enumerated objects, use the toInt() library function:

let item = Item.get("filthy lucre");
let itemId = toInt(item);

Other

  • ASH maps are converted to plain JS objects, and ASH arrays are converted to JS arrays.
  • You can look at the type reference for the JS version of the ASH runtime library with jsref, which works just like ashref.

ASH and JavaScript Interoperability

JavaScript scripts can require() ASH scripts and use their functions. When an ASH script is require()-ed by JavaScript code, KoLmafia will execute top-level code, but only export functions in the top-level scope. ASH variables are not exported.

For example, you can use Zlib if it is installed:

const { getvar } = require("zlib.ash");
let myvar = getvar("SOME_VAR_NAME");

ASH scripts cannot import JavaScript scripts.

JavaScript Version and Features

KoLmafia uses the Rhino engine to execute JavaScript code. Rhino supports an older version of JavaScript called "ES5", plus some features from newer versions. This means that many JavaScript features that work in web browsers might not work in KoLmafia.

Here is an incomplete list of post-ES5 features supported by Rhino (source):

Supported

  • Syntax
    • let and (partially) const
      • Does not support block-level scoping or temporal dead zones, meaning that you cannot use const for loop variables. for (const a in obj) { ... } is a syntax error.
    • Array/object destructuring (but spread/rest syntax (...) is not supported)
    • for...of loop
    • Arrow functions: () => {}
    • Octal and binary literals
  • Features
    • Symbol
    • Set, Map, WeakSet, WeakMap
    • ES2015 methods in Array, Math, Number, Object, String
    • Array.prototype.includes()
    • String.prototype.padStart()/padEnd()/trimStart()/trimEnd()
    • TypedArray: Can be constructed, but most TypedArray-specific methods are unavailable.

Unsupported

  • Syntax
    • Spread/rest syntax (...)
    • Object destructuring in assignments (variable declarations are OK)
    • Template string literals: Backtick string literals (``) are not a syntax error, but are treated as plain string literals.
    • Classes
    • ECMAScript modules (import/export)
    • Default function parameters
    • Computed property names
    • Shorthand property names (in object literals)
    • Exponentiation operator (**)
    • Async/Await
    • Trailing commas in function definitions (oddly, trailing commas are supported in function calls)
  • Features
    • Promise
    • Proxy
    • Reflect

Other

Most JavaScript globals available in browsers and/or server-side environments like Node.js are not available. This includes alert(), console.log(), and setTimeout().

Code Checking

You can set up ESLint to check your code for errors.

The following ESLint configuration (.eslintrc.json) checks if your JavaScript code uses features unsupported by KoLmafia, and prevents ESLint from complaining about KoLmafia builtins.

{
  "env": {
    "commonjs": true,
    "es6": true
  },
  "globals": {
    "Promise": "off",
    "Proxy": "off",
    "Reflect": "off",
    "Bounty": "readonly",
    "Class": "readonly",
    "Coinmaster": "readonly",
    "Effect": "readonly",
    "Element": "readonly",
    "Familiar": "readonly",
    "Item": "readonly",
    "Location": "readonly",
    "Monster": "readonly",
    "Phylum": "readonly",
    "Servant": "readonly",
    "Skill": "readonly",
    "Slot": "readonly",
    "Stat": "readonly",
    "Thrall": "readonly"
  },
  "parserOptions": {
    "ecmaVersion": 6
  },
  "rules": {
    "no-restricted-syntax": [
      "error",
      {
        "selector": "AssignmentExpression > ObjectPattern",
        "message": "Rhino does not support object destructuring in assignments (variable declarations are OK)"
      },
      {
        "selector": "AssignmentPattern",
        "message": "Rhino does not support default values for function parameters and array/object destructuring"
      },
      {
        "selector": "ClassDeclaration, ClassExpression",
        "message": "Rhino does not support ES2015 classes"
      },
      {
        "selector": ":matches(ForInStatement, ForOfStatement, ForStatement) > VariableDeclaration[kind=const]",
        "message": "Rhino does not support const declarations in the head of for-loops"
      },
      {
        "selector": "ObjectExpression > Property[shorthand=true]",
        "message": "Rhino does not support shorthand object properties"
      },
      {
        "selector": "Property[computed=true]",
        "message": "Rhino does not support computed object properties"
      },
      {
        "selector": "RestElement, SpreadElement",
        "message": "Rhino does not support spread/rest syntax"
      },
      {
        "selector": "TemplateLiteral",
        "message": "Rhino does not support template string literals"
      }
    ]
  }
}

Note that this is unnecessary if you use a transpiler to convert modern JavaScript code to Rhino-compatible syntax.

Transpiling

Various tools as Babel, Webpack, and TypeScript can convert modern JavaScript code (or another programming language) to legacy syntax supported by Rhino. These tools are called "transpilers". These tools allow you to enjoy the convenience and safety of modern language features, while still generating code that runs on KoLmafia.

Babel

Babel 7.15.0 supports Rhino as a compilation target.

The following config file (babel.config.json) will convert modern JavaScript syntax to Rhino-compatible ES5+:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "modules": false,
        "targets": {
          "rhino": "1.7.13"
        }
      }
    ]
  ]
}
  • Babel: As of r20558, you will still need to apply several patches to Babel in order to get babel-preset-env to work. See [1] for an example of a working Babel/Webpack/Typescript configuration; you'll need the configuration files and also the patches, which can be applied with patch-package.

TypeScript

TypeScript is a statically typed programming language that can be transpiled to JavaScript.

If you use TypeScript without Babel, you can set the target to "ES5".

TypeScript does not provide polyfills for modern JavaScript APIs missing in Rhino. To use them, you must supply your own.

To avoid accidentally using any missing APIs, we recommend using the following configuration for your tsconfig.json:

{
  "compilerOptions": {
    // JavaScript APIs supported by Rhino 1.7.13
    // See https://mozilla.github.io/rhino/compat/engines.html for more info
    "lib": [
      "ES5",
      "ES2015.Collection",
      "ES2015.Core",
      "ES2015.Generator",
      "ES2015.Iterable",
      "ES2015.Symbol",
      "ES2015.Symbol.WellKnown",
      "ES2016.Array.Include",
      "ES2017.String",
      "ES2019.String"
    ],
    // Rhino uses require() instead of import/export
    // Note: If you use Webpack or Rollup, change this to "ES2015"
    "module": "CommonJS",
    // Rhino supports ES5+
    // Note: If you use Babel with TypeScript, change this to "ESNext"
    "target": "ES5"
  }
}

If you use a bundler such as Webpack or Rollup, you should change the module to "ES2015", and let the bundlers convert your code to CommonJS.

History

  • r20520: Lifecycle scripts now support JavaScript.
  • r20620: Enumerated objects no longer provide a custom valueOf() method. Use toInt() instead.
  • r20780: When calling ASH runtime library functions from JavaScript, functions that return a boolean will return false on failure instead of throwing a string as an exception.