JavaScript Support: Difference between revisions
→Basics: Add "Library Functions" and "Importing and Exporting" subsections |
Ryo Sangnoir (talk | contribs) →Library Functions: neat trick for javascript scripts |
||
(9 intermediate revisions by one other user 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 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.''' | ||
== Basics == | == Basics == | ||
Line 13: | Line 13: | ||
const kolmafia = require("kolmafia"); | const kolmafia = require("kolmafia"); | ||
kolmafia.print("Hello, " + kolmafia.myName()); | kolmafia.print("Hello, " + kolmafia.myName()); | ||
// Alternative to have all functions in the global namespace | |||
Object.assign(globalThis, require("kolmafia")); | |||
print("Hello, " + myName()); | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Line 38: | Line 42: | ||
> js print("My name is " + myName()) | > js print("My name is " + myName()) | ||
My name is <playername> | My name is <playername> | ||
=== Importing and Exporting === | === Importing and Exporting === | ||
Line 84: | Line 86: | ||
Enumerated objects support the <code>toString()</code> method, which acts like the {{f|to_string}} ASH function. | 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"> | <syntaxhighlight lang="js"> | ||
let item = Item.get("filthy lucre"); | let item = Item.get("filthy lucre"); | ||
Line 133: | Line 134: | ||
* Syntax | * Syntax | ||
** Spread/rest syntax (<code>...</code>) | ** 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. | ** Template string literals: Backtick string literals (<code>``</code>) are not a syntax error, but are treated as plain string literals. | ||
** Classes | ** Classes | ||
Line 138: | Line 140: | ||
** Default function parameters | ** Default function parameters | ||
** Computed property names | ** Computed property names | ||
** Shorthand property names (in object literals) | |||
** Exponentiation operator (<code>**</code>) | |||
** Async/Await | ** Async/Await | ||
** Trailing commas in function definitions (oddly, trailing commas are supported in function ''calls'') | ** Trailing commas in function definitions (oddly, trailing commas are supported in function ''calls'') | ||
Line 147: | Line 151: | ||
=== Other === | === 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>. | 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 == | == 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. | * 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. | ||
=== TypeScript === | === [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>. | If you use TypeScript without Babel, you can set the <code>[https://www.typescriptlang.org/tsconfig#target target]</code> to <kbd>"ES5"</kbd>. | ||
Line 189: | Line 295: | ||
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. | 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)
andItem.get("5")
return the same result.
- For example,
<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[]
).
- For example,
<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.
- For example,
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 likeashref
.
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.
- Does not support block-level scoping or temporal dead zones, meaning that you cannot use
- 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)
- Spread/rest syntax (
- 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. UsetoInt()
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.