diff --git a/README.md b/README.md index bb31abc5..d5450137 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern ### Installation -``` +```bash $ npm install ``` ### Local Development -``` +```bash $ npm run start ``` diff --git a/docs/advanced/compiler-annotations.md b/docs/advanced/compiler-annotations.md new file mode 100644 index 00000000..5f6c151c --- /dev/null +++ b/docs/advanced/compiler-annotations.md @@ -0,0 +1,667 @@ +--- +title: Compiler Annotations +--- + +import { SideBySide } from "@site/src/components/SideBySide"; + +To improve translation and compatibility to different Lua interfaces, the TypeScriptToLua transpiler supports several custom annotations that slightly change translation results. This page documents the supported annotations. The syntax of the compiler annotations use the JSDoc syntax. + +## @compileMembersOnly + +**Target elements:** `(declare) enum` + +This decorator removes an enumeration's name after compilation and only leaves its members. Primarily used for APIs with implicit enumerations. + +**Example** + + + +```typescript +declare enum MyEnum { + MY_ENUM_MEMBER_A, + MY_ENUM_MEMBER_B, +} + +print(MyEnum.MY_ENUM_MEMBER_A); +``` + +```lua +print(MyEnum.MY_ENUM_MEMBER_A) +``` + + + + + +```typescript +/** @compileMembersOnly */ +declare enum MyEnum { + MY_ENUM_MEMBER_A, + MY_ENUM_MEMBER_B, +} + +print(MyEnum.MY_ENUM_MEMBER_A); +``` + +```lua +print(MY_ENUM_MEMBER_A) +``` + + + +**Example 2** + + + +```typescript +enum MyEnum { + MY_ENUM_MEMBER_A, + MY_ENUM_MEMBER_B, + MY_ENUM_MEMBER_C = "c", +} + +print(MyEnum.MY_ENUM_MEMBER_A); +``` + +```lua +MyEnum = {} +MyEnum.MY_ENUM_MEMBER_A = 0 +MyEnum.MY_ENUM_MEMBER_B = 1 +MyEnum.MY_ENUM_MEMBER_C = "c" + +print(MyEnum.MY_ENUM_MEMBER_A) +``` + + + + + +```typescript +/** @compileMembersOnly */ +enum MyEnum { + MY_ENUM_MEMBER_A, + MY_ENUM_MEMBER_B, + MY_ENUM_MEMBER_C = "c", +} + +print(MyEnum.MY_ENUM_MEMBER_A); +``` + +```lua +MY_ENUM_MEMBER_A = 0 +MY_ENUM_MEMBER_B = 1 +MY_ENUM_MEMBER_C = "c" + +print(MY_ENUM_MEMBER_A) +``` + + + +## @customConstructor + +**Target elements:** `declare class` + +Changes the way new instances of this class are made. Takes exactly one argument that is the name of the alternative constructor function. + +**Example** + + + +```typescript +declare class MyClass { + constructor(x: number); +} +const inst = new MyClass(3); +``` + +```lua +local inst = __TS__New(MyClass, 3) +``` + + + + + +```typescript +/** @customConstructor MyConstructor */ +declare class MyClass { + constructor(x: number); +} +const inst = new MyClass(3); +``` + +```lua +local inst = MyConstructor(3) +``` + + + +## @extension + +**Target elements:** `class` + +The Extension decorator marks a class as an extension of an already existing class. This causes the class header to not be translated, preventing instantiation and the override of the existing class. + +**Example** + + + +```typescript +class MyBaseClass { + myFunction(): void {} +} +``` + +```lua +MyBaseClass = __TS__Class() +... +function MyBaseClass.prototype.myFunction(self) end +``` + + + + + +```typescript +/** @extension */ +class MyBaseClass { + myFunction(): void {} +} +``` + +```lua +function MyBaseClass.myFunction(self) end +``` + + + +## @forRange + +**Target elements:** `declare function` + +Denotes a function declaration is a Lua numerical iterator. When used in a TypeScript `for...of` loop, the resulting Lua will use a numerical for loop. + +The function should not be a real function and an error will be thrown if it is used in any other way. + +**Example** + + + + +```typescript +/** @forRange */ +declare function forRange(start: number, limit: number, step?: number): number[]; + +for (const i of forRange(1, 10)) {} +for (const i of forRange(10, 1, -1)) {} +``` + +```lua +for i = 1, 10 do end +for i = 10, 1, -1 do end +``` + + + +## @luaIterator + +**Target elements:** `(declare) interface` + +Denotes a type is a Lua iterator. When an object of a type with this annotation is used in a for...of statement, it will transpile directly as a lua iterator in a for...in statement, instead of being treated as a TypeScript iterable. Typically, this is used on an interface that extends `Iterable` or `Array` so that TypeScript will allow it to be used in a for...of statement. + +**Example** + + + + +```typescript +/** @luaIterator */ +type LuaIterable = Iterable; + +declare function myIterator(): LuaIterator; +for (const s of myIterator()) {} +``` + +```lua +for s in myIterator() do end +``` + + + +This can also be combined with [@tupleReturn](#tuplereturn), if the iterator returns multiple values. + +**Example** + + + + +```typescript +/** @luaIterator @tupleReturn */ +type LuaTupleIterable = Iterable; + +declare namespace string { + function gmatch(s: string, pattern: string): LuaTupleIterable; +} + +for (const [a, b] of string.gmatch("foo", "(.)(.)")) {} +``` + +```lua +for a, b in string.gmatch("foo", "(.)(.)") do end +``` + + + +## @luaTable + +**Target elements:** `type` + +This annotation signals the transpiler to translate a class as a simple lua table for optimization purposes. + +```ts +/** @luaTable */ +declare class Table { + readonly length: number; + set(key: K, value: V | undefined): void; + get(key: K): V | undefined; +} + +const tbl = new Table(); // local tbl = {} + +const foo = {}; +tbl.set(foo, "bar"); // tbl[foo] = "bar" +print(tbl.get(foo)); // print(tbl[foo]) + +tbl.set(1, "baz"); // tbl[1] = "baz" +print(tbl.length); // print(#tbl) +``` + +## @metaExtension + +**Target elements:** `class` + +The Extension decorator marks a class as an extension of an already existing meta class/table. This causes the class header to not be translated, preventing instantiation and the override of the existing class. + +**Example** + + + +```typescript +class MyBaseClass { + myFunction(): void {} +} +``` + +```lua +MyBaseClass = __TS__Class() +... +function MyBaseClass.prototype.myFunction(self) end +``` + + + + + +```typescript +/** @metaExtension */ +class MyMetaExtension extends MyMetaClass { + myFunction(): void {} +} +``` + +```lua +local __meta__MyMetaClass = debug.getregistry().MyMetaClass +__meta__MyMetaClass.myFunction = function(self) +end; +``` + + + +## @noResolution + +**Target elements:** `module` + +Prevents tstl from trying to resolve the module path. When importing this module the path will be exactly the path in the import statement. + +**Example** + + + +```typescript +declare module "mymodule" {} +import module from "mymodule"; +``` + +```lua +... +local module = require("src.mymodule"); +``` + + + + + +```typescript +/** @noResolution */ +declare module "mymodule" {} +import module from "mymodule"; +``` + +```lua +... +local module = require("mymodule"); +``` + + + +## @noSelf + +**Target elements:** `declare class`, `(declare) interface` or `declare namespace` + +Indicates that functions inside a scope do not take in initial `self` argument when called, and thus will be called with a dot `.` instead of a colon `:`. It is the same as if each function was declared with an explicit `this: void` parameter. Functions that already have an explicit `this` parameter will not be affected. + +When applied to a class or interface, this only affects the type's declared methods (including static methods and fields with a function type). It will not affect other function declarations, such as nested functions inside a class' methods. + +**Example** + + + +```typescript +declare interface NormalInterface { + normalMethod(s: string): void; +} +declare const x: NormalInterface; + +/** @noSelf **/ +declare interface NoSelfInterface { + noSelfMethod(s: string): void; +} +declare const y: NoSelfInterface; + +x.normalMethod("foo"); +y.noSelfMethod("bar"); +``` + +```lua +x:normalMethod("foo") +y.noSelfMethod("bar") +``` + + + +When applied to a namespace, all functions declared within the namespace will treated as if they do not have a `self` parameter. In this case, the effect is recursive, so functions in nested namespaces and types declared as parameters will also be affected. + +**Example** + + + +```typescript +declare namespace NormalNS { + function normalFunc(s: string): string; +} + +/** @noSelf **/ +declare namespace NoSelfNS { + function noSelfFunc(s: string): string; +} + +NormalNS.normalFunc("foo"); +NoSelfNS.noSelfFunc("bar"); +``` + +```lua +NormalNS:normalFunc("foo") +NoSelfNS.noSelfFunc("bar") +``` + + + +For more information about how the `self` parameter is handled, see [Functions and the `self` Parameter](functions-and-the-self-parameter.md) + +## @noSelfInFile + +**Target elements:** `(declare) file` + +Indicates that functions in a file do not take in initial `self` argument when called. + +This is annotation works the same as [@noSelf](#noself) being applied to a namespace, but affects the entire file. + +`@noSelfInFile` must be placed at the top of the file, before the first statement. + +## @phantom + +**Target elements:** `namespace` + +This decorator marks a namespace as a phantom namespace. This means all members of the namespace will be translated as if they were not in that namespace. Primarily used to prevent scoping issues. + +**Example** + + + +```typescript +namespace myNameSpace { + function myFunction(): void {} +} +``` + +```lua +myNameSpace = {} +function myNameSpace.myFunction() end +``` + + + + + +```typescript +/** @phantom */ +namespace myNameSpace { + function myFunction(): void {} +} +``` + +```lua +function myFunction() end +``` + + + +## @pureAbstract + +**Target elements:** `declare class` + +This decorator marks a class declaration as purely abstract. The result is that any class extending the purely abstract class will not extend this class in the resulting Lua. + +**Example** + + + +```typescript +declare class MyAbstractClass {} +class MyClass extends MyAbstractClass {} +``` + +```lua +MyClass = __TS__Class() +MyClass.__base = MyAbstractClass +MyClass.____super = MyAbstractClass +setmetatable(MyClass, MyClass.____super) +setmetatable(MyClass.prototype, MyClass.____super.prototype) +``` + + + + + +```typescript +/** @pureAbstract */ +declare class MyAbstractClass {} +class MyClass extends MyAbstractClass {} +``` + +```lua +MyClass = __TS__Class() +``` + + + +## @tupleReturn + +**Target elements:** `(declare) function` + +This decorator indicates a function returns a lua tuple instead of a table. It influences both destructing assignments of calls of that function, as well as changing the format of returns inside the function body. + +**Example** + + + +```typescript +function myFunction(): [number, string] { + return [3, "4"]; +} +const [a, b] = myFunction(); +``` + +```lua +function myFunction() + return {3, "4"} +end +local a,b = unpack(myFunction()) +``` + + + + + +```typescript +/** @tupleReturn */ +function myFunction(): [number, string] { + return [3, "4"]; +} +const [a, b] = myFunction(); +``` + +```lua +function myFunction() + return 3, "4" +end +local a, b = myFunction() +``` + + + +If you wish to use this annotation on function with overloads, it must be applied to each signature that requires it. + +**Example** + +```typescript +/** @tupleReturn */ +declare function myFunction(s: string): [string, string]; +/** @tupleReturn */ +declare function myFunction(n: number): [number, number]; +``` + +Note that if any overloaded signature of a function implementation has the annotation, all array/tuple return values will unpacked in the transpiled output. + +## @vararg + +**Target elements:** `(declare) interface or type` + +Indicates that an array-like type represents a Lua vararg expression (`...`) and should be transpiled to that when used in a spread expression. This is useful for forwarding varargs instead of wrapping them in a table and unpacking them. + +**Example** + + + +```typescript +function varargWrapUnpack(...args: string[]) { + console.log(...args); +} +``` + +```lua +function varargWrapUnpack(self, ...) + local args = ({...}) + print(unpack(args)) +end +``` + + + + + +```typescript +/** @vararg */ +interface Vararg extends Array {} + +function varargForward(...args: Vararg) { + console.log(...args); +} +``` + +```lua +function varargForward(self, ...) + print(...)) +end +``` + + + +This can be used to access the file-scope varargs as well. + +**Example** + + + +```typescript +declare const arg: Vararg; +console.log(...arg); +const [x, y] = [...arg]; +``` + +```lua +print(...) +local x, y = ... +``` + + + +To also support tuple-typed rest parameters, you can define the type like this: + +**Example** + +```typescript +/** @vararg */ +type Vararg = T & { __luaVararg?: never }; + +function varargForward(...args: Vararg<[string, number]>) {} +``` + +**_Warning_** + +TypeScriptToLua does not check that the vararg expression is valid in the context it is used. If the array is used in a spread operation in an invalid context (such as a nested function), a deoptimization will occur. + +**Example** + + + +```typescript +function outerFunction(...args: Vararg) { + function innerFunction() { + console.log(...args); + } + innerFunction(); +} +``` + +```lua +function outerFunction(self, ...) + local args = {...} + local function innerFunction(self) + print(unpack(args)) + end + innerFunction(_G) +end +``` + + diff --git a/docs/advanced/functions-and-the-self-parameter.md b/docs/advanced/functions-and-the-self-parameter.md new file mode 100644 index 00000000..b0d139fc --- /dev/null +++ b/docs/advanced/functions-and-the-self-parameter.md @@ -0,0 +1,234 @@ +--- +title: Functions and the `self` Parameter +--- + +import { SideBySide } from "@site/src/components/SideBySide"; + +## Every Function Has a Context Parameter + +In JavaScript and TypeScript, almost all functions have access to an implicit `this` parameter. In order to maintain compatibility with this, all Lua functions are generated with an extra initial context parameter. + +**Example** + + + +```typescript +function myFunction(arg: string) {} +myFunction("foo"); +``` + +```lua +function myFunction(self, arg) +end +myFunction(nil, "foo") +``` + + + +The reason for this is that a method can be assigned to a stand-alone function and vice-versa. + +**Example** + +```typescript +class MyClass { + myMethod(arg: string) { + console.log("myMethod", arg); + } +} + +let myFunction = function(arg: string) { + console.log("myFunction", arg); +}; + +const c = new MyClass(); + +c.myMethod = myFunction; +c.myMethod("foo"); // should output: myFunction foo +// or +myFunction = c.myMethod; +myFunction("foo"); // should output: myMethod foo; +``` + +If `myFunction` did not have the initial parameter, calling either after being re-assigned would cause potential runtime errors, since `myMethod` would expect an initial parameter and `myFunction` would not. + +Note that even declared functions are assumed to have this extra parameter as well. + +**Example** + + + +```typescript +declare function myLibFunction(arg: string): void; +myLibFunction("foo"); +``` + +```lua +myLibFunction(nil, "foo") +``` + + + +## Removing the Context Parameter + +When dealing with external library functions that don't expect this initial parameter, you will need to inform TypeScriptToLua. This can be done a few different ways. + +### `this: void` + +You can declare any function with `this: void` to prevent generation of this initial argument. + +**Example** + + + +```typescript +declare function myLibFunction(this: void, arg: string): void; +myLibFunction("foo"); +``` + +```lua +myLibFunction("foo") +``` + + + +This works on methods as well, which can be useful if you have class methods which should be called with a dot `.` instead of a colon `:`. + +**Example** + + + +```typescript +declare class MyClass { + withContext(arg: string): void; + withoutContext(this: void, arg: string): void; +} +const c = new MyClass(); +c.withContext("foo"); +c.withoutContext("foo"); +``` + +```lua +local c = __TS__New(MyClass) +c:withContext("foo") -- uses colon : +c.withoutContext("foo") -- uses dot . +``` + + + +Another common scenario is a library function which takes a lua callback function, which should not have a context parameter. + +**Example** + + + + +```typescript +declare function takesCallback( + this: void, + callback: (this: void, arg: string) => void, +): void; + +takesCallback(arg => { + console.log(arg); +}); +``` + +```lua +takesCallback(function(arg) print(arg) end) +``` + + + +### `@noSelf` + +If you wish to specify that all functions in a class, interface or namespace should not have a context parameter, you can use the [`@noSelf`](compiler-annotations.md#noself) annotation. + +**Example** + + + +```typescript +/** @noSelf **/ +declare namespace MyNamespace { + function myFunction(arg: string): void; +} +MyNamespace.myFunction("foo"); +``` + +```lua +MyNamespace.myFunction("foo") +``` + + + +You can override `@noSelf` on a per-function basis by specifying a `this` parameter. + +**Example** + + + +```typescript +/** @noSelf **/ +declare namespace MyNamespace { + function myFunction(this: any, arg: string): void; +} +MyNamespace.myFunction("foo"); +``` + +```lua +MyNamespace:myFunction("foo") +``` + + + +### `@noSelfInFile` + +If you want to specify that all functions in a file should have no context, you can use [`@noSelfInFile`](compiler-annotations.md#noselfinfile) at the top of the file. + +For more information on [`@noSelf`](compiler-annotations.md#noself) and [`@noSelfInFile`](compiler-annotations.md#noselfinfile), please refer to [Compiler Annotations](compiler-annotations). + +## Assignment Errors + +Functions that have a context parameter cannot be assigned to functions that do not, and vice-versa. A common case where this may occur is passing a callback to an api that expects a function that does not take an initial argument. + +**Example** + +```ts +declare function takesCallback(callback: (this: void, arg: string) => void); + +function myCallback(arg: string) {} +takesCallback(myCallback); // Error: Unable to convert function with a 'this' parameter to function with no 'this'. To fix, wrap in an arrow function, or declare with 'this: void'. +``` + +This throws an error because if `takesCallback` called `myCallback`, it would do so without passing an initial context parameter. This can be easily fixed simply by wrapping the call in an arrow function. + +**Example** + + + +```typescript +takesCallback(arg => myCallback(arg)); +``` + +```lua +takesCallback(function(arg) return myCallback(nil, arg) end) +``` + + + +The reason this works is because TypeScriptToLua infers whether the arrow function should take a context parameter or not based on the type it's being assigned to. + +### Overloads + +If a function is overloaded and the signatures differ in context type, you can not assign them: + +```ts +declare function takesFunction(f: Function): void; + +declare function myFunction(this: void, s: string, n: number): void; +declare function myFunction(s: string); + +takesFunction(myFunction); // Error: Unsupported assignment of function with different overloaded types for 'this'. Overloads should all have the same type for 'this'. +``` + +It's best practice to avoid overloads with different context types. diff --git a/docs/advanced/writing-declarations.md b/docs/advanced/writing-declarations.md new file mode 100644 index 00000000..2c359032 --- /dev/null +++ b/docs/advanced/writing-declarations.md @@ -0,0 +1,625 @@ +--- +title: Writing Declarations +--- + +The real power of the transpiler is unlocked when combining it with declarations for your target environment. Declarations tell TypeScript which Lua API is available in your target context. + +If you need tips or help writing declarations, feel free to [join our Discord](https://discord.gg/BWAq58Y). + +## About Declaration Files + +Declaration files end with the extension _.d.ts_. These contain pure ambient code. + +For TypeScriptToLua, these files should contain information that describes the target Lua environment. + +This means functions, modules, variables and other members of the target Lua environment are primarily described in these files. + +They don't contain code that you would execute. Similar to how you'd write an interface in some other languages. TypeScriptToLua doesn't output any information from these files either. + +:::note +You can write ambient declarations inside _.ts_ files as well. +::: + +## Declare Keyword + +The `declare` keyword is used to say that the following declaration defines something that exists within global scope. Like something within the `_G` table in Lua. + +This is useful for defining Lua's environment. + +```ts title=_G.d.ts +// Uses some declarations from +// https://www.lua.org/manual/5.1/manual.html + +/** + * A global variable (not a function) that holds a string containing the + * current interpreter version. + */ +declare const _VERSION: number; + +/** + * Receives any number of arguments, and prints their values to stdout, using the + * tostring function to convert them to strings. print is not intended for + * formatted output, but only as a quick way to show a value, typically for + * debugging. For formatted output, use string.format. + * @param args Arguments to print + */ +declare function print(...args: any[]): void; +``` + +```ts title=main.ts +print(_VERSION); // Editor and transpiler know what print and _VERSION are +``` + +:::note +You can use `declare` to write ambient declarations inside _.ts_ files. +::: + +## Export Keyword + +The export keyword indicates something is exported and can be used by external code. + +This also includes ambient interfaces, types, modules and other items that don't result in any transpiled code. + +If a file named _lib.lua_ exists and returns a table with an `x` field, you can write _lib.d.t.s_ as follows to tell TypeScript that _lib_ exists and what it provides. + +```ts title=lib.d.ts +export let x: number; +``` + +```ts title=main.ts +import { x } from "./lib"; +``` + +If a namespace contains certain functions, `export` tells TypeScript that those functions can be accessed within the namespace. + +```ts title=table.d.ts +declare namespace table { + /** + * @noSelf + */ + export function insert(table: object, item: any): number; +} +``` + +```ts title=main.ts +table.insert({}, 1); +``` + +If a globally available module exists within the Lua environment. You can define what the module provides. + +```ts title=utf8.d.ts +declare module "utf8" { + /** + * @noSelf + */ + export function codepoint(): void; +} +``` + +```ts title=main.ts +import * as utf8 from "utf8"; // equiv to `local utf8 = require("utf8"); +utf8.codepoint(); +``` + +The `export` keyword can be used in a `.ts` or `.d.ts` file. It tells the transpiler and your editor (potentially) that something **contains/provides** something that you can either import (by using `import` in TS or `require()` in Lua) or access. + +## Self Parameter + +TypeScript has a hidden `this` parameter attached to every function. + +This causes TypeScriptToLua to treat every function as if `self` exists as its first parameter. + +```ts +declare function assert(value: any): void; +// TypeScript: assert(this: any, value: any): void; +// TypeScriptToLua: assert(self, value) +assert(true); // assert(_G, true) +``` + +This allows users to modify `this` inside a function and expect behaviour similar to what JavaScript does. + +But obviously Lua does not have a `self` parameter for every function, so one of the three options must happen to tell TypeScriptToLua there is no "contextual parameter" (`self`): + +1. Use `this: void` as the first parameter of the function / method. This formally describes to TypeScript to not allow `this` to be modified inside this function. (you could also use the [noImplicitThis](../configuration.md#custom-options) option to disallow `this` to be modified if `this` is of an `any` type). +2. Use `@noSelf` in the comments of the declaration's owner (the namespace, module, object, etc). +3. Use `@noSelfInFile` at the beginning of the file in a comment to make sure every function defined in this file does not use a "contextual parameter". + +Below is three ways to make `table.remove` not use a "contextual parameter". + +```ts +declare namespace table { + export function remove(this: void, table: object, index: number): any; +} +``` + +```ts +/** @noSelf */ +declare namespace table { + export function remove(table: object, index: number): any; +} +``` + +```ts +/** @noSelfInFile */ + +declare namespace table { + export function remove(table: object, index: number): any; +} +``` + +By doing this, the transpiler also figures out if it needs to use `:` or `.` when invoking a function / method. + +## Comments and Annotations + +If you're using an editor that seeks out information about functions, variables, etc. It will likely find the file where what it is analyzing is defined and check out the comment above it. + +```ts +/** + * When hovering over print, this description will be shown + * @param args Stuff to print + */ +declare function print(...args: any[]); +``` + +

Try out what this looks like in an editor

+ +TypeScript uses [TSDoc](https://github.com/microsoft/tsdoc) for its comments. TSDoc allows you to also use markdown in your comments! This means pictures, links, tables, code syntax highlighting and more markdown features are available. These may display differently depending on the editor in use. + +Here are some commonly used TSDoc tags used in comments: + +| Tag | Description | +| ----------------------------- | ---------------------------------------------------- | +| `@param ` | Defines a parameter. e.g. A parameter for a function | +| `@return ` | Describes the return value of a function / method | + +TypeScriptToLua takes this further. Some "tags" change how the transpiler translates certain pieces of code. These are referred to as [annotations](compiler-annotations.md). + +As an example, `@tupleReturn` marks a function as something which returns multiple values instead of its array. + +```ts +/** + * Returns multiple values + * @tupleReturn + */ +declare function tuple(): [number, number]; + +let [a, b] = tuple(); +// local a, b = tuple() +``` + +```ts +/** + * Returns a table array containing two numbers + */ +declare function array(): [number, number]; + +let [c, d] = array(); +// local c, d = unpack(array()) +``` + +See [Compiler Annotations](compiler-annotations.md) page for more information. + +## Environmental Declarations + +By default, TypeScript includes global type declarations for both ECMAScript and web standards. TypeScriptToLua aims to support only standard ECMAScript feature set. To make TypeScript not suggest you to use unsupported browser builtins (including `window`, `document`, `console`, `setTimeout`) you can specify a `lib` option: + +```json title=tsconfig.json +{ + "compilerOptions": { + "lib": ["esnext"] + } +} +``` + +It is also possible to use `noLib` to remove every standard declaration (to use TypeScriptToLua only for syntactic features with Lua standard library) but TypeScript **needs** certain declarations to exist so they will have to be manually defined, so using `noLib` is not recommended. + +## Advanced Types + +We recommend reading about Mapped and Conditional types. These things can be used as effective tools to describe some dynamic things that you may have in Lua. + +- [Advanced Types (TypeScriptLang)](https://www.typescriptlang.org/docs/handbook/advanced-types.html#) + - [Mapped Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types) + - [Conditional Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types) + +## Declaration Merging + +https://www.typescriptlang.org/docs/handbook/declaration-merging.html + +Some examples of declaration merging have been shown in the above examples. + +### Function + Table + +Some tables can use `__call` to make themselves callable. Busted (the Lua testing suite) does this to `assert`. + +```ts title=assert.d.ts +declare function assert(value: any, errorDescription?: string): void; +declare namespace assert { + export function isEqual(): void; +} +``` + +```ts title=main.ts +assert.isEqual(); +assert(); +``` + +## Declaration Examples + +### Interfaces + +```ts title=image.d.ts +interface Image { + /** @tupleReturn */ + getDimensions(): [number, number]; +} + +// This interface merges with its previous declaration +/** @noSelf */ +interface Image { + getFlags(): object; +} +``` + +```ts title=main.ts +declare let image: Image; +let [w, h] = image.getDimensions(); // local w, h = image:getDimensions() +let o = image.getFlags(); +``` + +### Namespaces + +```ts title=love.d.ts +declare namespace love { + export let update: (delta: number) => void; + /** @tupleReturn */ + export function getVersion(delta: number): [number, number, number, string]; + export namespace graphics { + function newImage(filename: string): Image; + } +} + +// This namespace merges with its previous declaration +/** @noSelf */ +declare namespace love { + export let update: (delta: number) => void; +} + +/** @noSelf */ +declare namespace string { + function byte(s: string, i?: number, j?: number): number; +} +``` + +```ts title=main.ts +let [a, b, c, d] = love.getVersion(); +let p = love.graphics.newImage("file.png"); +``` + +### Classes + +Because Lua doesn't have a strictly defined concept of a class, for TypeScriptToLua `class` declaration implies a very specific structure, built specifically for TypeScript compatibility. Because of that, usually you shouldn't use `declare class` for values coming from Lua. + +Most of Lua patterns used to simulate classes can be declared using interfaces instead. + +**Example 1**: a table with a static `new` method to construct new instances + +```lua +Box = {} +Box.__index = Box + +function Box.new(value) + local self = {} + setmetatable(self, Box) + self._value = value + return self +end + +function Box:get() + return self._value +end +``` + +```ts +interface Box { + get(): string; +} + +interface BoxConstructor { + new: (this: void, value: string) => Box; +} + +declare var Box: BoxConstructor; + +// Usage +const box = Box.new("foo"); +box.get(); +``` + +**Example 2**: a callable table with extra static methods + +```lua +Box = {} + +local instance +function Box:getInstance() + if instance then return instance end + instance = Box("instance") + return instance +end + +setmetatable(Box, { + __call = function(_, value) + return { get = function() return value end } + end +}) +``` + +```ts +interface Box { + get(): string; +} + +interface BoxConstructor { + (this: void, value: string): Box; + getInstance(): Box; +} + +declare var Box: BoxConstructor; + +// Usage +const box = Box("foo"); +box.get(); +Box.getInstance().get(); +``` + +### Ambient Modules + +You may have to use the `@noResolution` annotation to tell TypeScriptToLua to not try any path resolution methods when the specified module is imported. + +Module declarations need to be kept in _.d.ts_ files. + +```ts title=types.d.ts +/** @noSelf */ +declare module "image-size" { + export function getimagewidth(filename: string): number; + export function getimageheight(filename: string): number; +} + +/** + * A module that only contains a number + * @noResolution + */ +declare module "number-of-the-day" { + let x: number; + export = x; +} + +/** + * Not very useful for TypeScript. It has no idea what is in here. + * @noResolution + */ +declare module "custom-module"; +``` + +```ts title=main.ts +import { getimagewidth, getimageheight } from "image-size"; +import * as x from "number-of-the-day"; +import * as customModule from "custom-module"; +``` + +### Unions + +Unions can be used to tell TypeScript that a given type could be one of many other types. TypeScript can then pick up hints in the code to figure out what that type is at a given statement. + + +```ts +declare interface PingResponse { + type: "ping"; + timeTaken: number; +} + +declare interface MessageResponse { + type: "message"; + text: string; +} + +declare type Response = PingResponse | MessageResponse; + +declare let response: Response; + +response.timeTaken; +// Not allowed, if response is a MessageResponse, it won't have a timeTaken field + +switch (response.type) { + case "ping": + // If the program arrives here, response: PingResponse + return response.timeTaken; + case "message": + // If the program arrives here, response: MessageResponse + return response.text; + case "disconnect": + // Impossible + default: + // Because of what Response is described as, TypeScript knows getting + // here is impossible. +} +``` + +### keyof + +```ts +declare interface AvailableFiles { + "player.png": any; + "file.txt": any; +} + +declare function getFile(filename: keyof AvailableFiles): string; + +getFile("player.png"); // Valid +getFile("unknown.png"); // Invalid +``` + +### Literal Types + +String and number values can be used as types too. In combination with union types it can be used to represent a known set of values. + +```ts +declare function drawLine(type: "solid" | "dashed"): void; +drawLine("solid"); // Valid +drawLine("rounded"); // Invalid +``` + +```ts +declare function getSupportedColors(): 1 | 8 | 256 | 16777216; +getSupportedColors() === 8; // Valid +getSupportedColors() === 16; // Invalid +``` + +### Keyword Workarounds + +Some functions in Lua can have names that are keywords in TypeScript (e.g., `try`, `catch`, `new`, etc). + +The parent to these kinds of functions will need to be represented as a JSON object. + +```ts +// ❌ +declare namespace table { + export function new: () => any; +} + +// ✔ +declare let table: { + new: () => any; +}; +``` + +```ts +// ❌ +declare module "creator" { + export function new: () => any; +} + +// ✔ +declare module "creator" { + let exports: { + new: () => any; + }; + export = exports; +} +``` + +### Operator Overloads + +Lua supports overloading of mathematical operators such as `+`, `-` or `*`. Since TypeScript does not support operator overloading in its type system this is hard to replicate. Unfortunately this is not something that can be fixed properly right now without forking off our custom TypeScript version. + +There is however a workaround that works decently: If you declare a type as intersection type with number it will inherit all mathematical operators. For example: + +```ts +declare type Vector = number & { + x: number; + y: number; + dot(v: Vector): number; + cross(v: Vector): Vector; +}; + +declare function Vector(x: number, y: number): Vector; + +const v1 = Vector(3, 4); +const v2 = Vector(4, 5); +const v3 = (v1 * 4) as Vector; +const d = v3.dot(v2); +``` + +### Import and export + +Using `import` can be important for making sure an _index.d.ts_ file contains all the declarations needed. + +```ts title=index.d.ts +import "./lib"; +// All global declarations in lib will be included with this file + +export { Player } from "./Entities"; +// The Player declaration is re-exported from this file +``` + +It is also possible to place `import` statements inside ambient modules and namespaces. + +```ts +declare module "mymodule" { + import * as types from "types"; + export function getType(): types.Type; +} +``` + +## NPM Publishing + +It is possible to publish a list of declarations for other users to easily download via [npm](https://www.npmjs.com/). + +```bash +npm init +npm login # Need npm account +npm publish --dry-run # Show what files will be published +npm version 0.0.1 # Update the version in package.json when --dry-run seems good +npm publish # Publish to npm (only if you're 100% sure) +``` + +Then the user can install this package using: + +```bash +npm install --save-dev +``` + +And link it to a _tsconfig.json_ file. + +```json title=tsconfig.json +{ + "compilerOptions": { + "types": ["declarations"] + } +} +``` + +## Debugging Declarations + +If you have TypeScript installed, you can use the command below to list all files a _tsconfig.json_ file targets. + +```bash +tsc -p tsconfig.json --noEmit --listFiles +``` + +This only works with TypeScript (_tsc_). TypeScriptToLua (_tstl_) may have support for this in the future. + +Every TypeScript project points to a list of declarations. TypeScript is very generous with what files that includes. + +```json title=tsconfig.json +{ + "compilerOptions": { + "rootDir": "src" + } +} +``` + +```diff + node_modules/ ++ src/main.ts ++ src/actors/Player.ts ++ global.ts + tsconfig.json +``` + +```json title=tsconfig.json +{ + "compilerOptions": { + "rootDir": "src", + "types": ["lua-types/jit"] + } +} +``` + +```diff ++ node_modules/lua-types/jit.d.ts ++ src/main.ts ++ src/actors/Player.ts ++ global.ts + tsconfig.json +``` diff --git a/docs/api/overview.md b/docs/api/overview.md new file mode 100644 index 00000000..1cc35648 --- /dev/null +++ b/docs/api/overview.md @@ -0,0 +1,124 @@ +--- +title: Overview +--- + +## High-level API + +The high level API allows you to simply invoke several common transpiler operations using well-known language primitives, handling usage of TypeScript API for you. + +### TranspileString + +Transpile a string containing TypeScript source code to Lua. + +**Arguments:** + +- Source: string - The TypeScript source code to transpile. +- _[Optional]_ Options: tstl.CompilerOptions - CompilerOptions to use. + +**Example:** + +```ts +import * as tstl from "typescript-to-lua"; + +const result = tstl.transpileString(`const foo = "bar";`, { luaTarget: tstl.LuaTarget.Lua53 }); +console.log(result.diagnostics); +console.log(result.file); +``` + +### TranspileFiles + +Transpile a collection of TypeScript files to Lua. + +**Arguments:** + +- FileNames: string[] - An array of file paths to the TypeScript files to be transpiled. +- _[Optional]_ Options: tstl.CompilerOptions - CompilerOptions to use. + +**Example:** + +```ts +import * as tstl from "typescript-to-lua"; + +const result = tstl.transpileFiles(["file1.ts", "file2.ts"], { luaTarget: tstl.LuaTarget.Lua53 }); +console.log(result.diagnostics); +console.log(result.emitResult); +``` + +### TranspileProject + +Transpile a TypeScript project to Lua. + +**Arguments:** + +- tsConfigPath: string - The file path to a TypeScript project's `tsconfig.json` file. +- _[Optional]_ extendedOptions: tstl.CompilerOptions - The tsConfig already contains options, this extends those options. + +**Example:** + +```ts +import * as tstl from "typescript-to-lua"; + +const result = tstl.transpileProject("tsconfig.json", { luaTarget: tstl.LuaTarget.Lua53 }); +console.log(result.diagnostics); +console.log(result.emitResult); +``` + +### TranspileVirtualProject + +Transpile a virtual project to Lua. A virtual project is a record (like an object literal for example) where keys are file names, and values are the contents of these files. This can be used to transpile a collection of files without having these files physically on disk. + +**Arguments:** + +- Files: Record - A record of fileName keys and fileContent values. +- _[Optional]_ Options: tstl.CompilerOptions - CompilerOptions to use. + +**Example:** + +```ts +import * as tstl from "typescript-to-lua"; + +const result = tstl.transpileVirtualProject( + { + "file1.ts": `const foo = "bar";`, + "file2.ts": `const bar = "baz";`, + }, + { luaTarget: tstl.LuaTarget.Lua53 }, +); +console.log(result.diagnostics); +console.log(result.transpiledFiles); +``` + +## Low-level API + +On the contrast with high-level API, low-level API requires you to to manage TypeScript project yourself. See [Using the Compiler API](https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API) page for the introduction to TypeScript API. + +### Transpile + +**Arguments:** + +- program: ts.Program - The TypeScript program to transpile (note: unlike the high-level API, compilerOptions is part of the program and cannot be supplied separately). +- _[Optional]_ sourceFiles: ts.SourceFile[] - A collection of `SourceFile`s to transpile, `program.getSourceFiles()` by default. +- _[Optional]_ customTransformers: ts.CustomTransformers - List of extra [TypeScript transformers](../configuration.md#transformers). +- _[Optional]_ plugins: tstl.Plugin[] - List of [TypeScriptToLua plugins](plugins.md). +- _[Optional]_ emitHost: tstl.EmitHost - Provides the methods for reading/writing files, useful in cases where you need something other than regular reading from disk. Defaults to `ts.sys`. + +**Example:** + +```ts +const reportDiagnostic = tstl.createDiagnosticReporter(true); +const configFileName = path.resolve(__dirname, "tsconfig.json"); +const parsedCommandLine = tstl.parseConfigFileWithSystem(configFileName); +if (parsedCommandLine.errors.length > 0) { + parsedCommandLine.errors.forEach(reportDiagnostic); + return; +} + +const program = ts.createProgram(parsedCommandLine.fileNames, parsedCommandLine.options); +const { transpiledFiles, diagnostics: transpileDiagnostics } = tstl.transpile({ program }); + +const emitResult = tstl.emitTranspiledFiles(options, transpiledFiles); +emitResult.forEach(({ name, text }) => ts.sys.writeFile(name, text)); + +const diagnostics = ts.sortAndDeduplicateDiagnostics([...ts.getPreEmitDiagnostics(program), ...transpileDiagnostics]); +diagnostics.forEach(reportDiagnostic); +``` diff --git a/docs/api/plugins.md b/docs/api/plugins.md new file mode 100644 index 00000000..fabaa2f7 --- /dev/null +++ b/docs/api/plugins.md @@ -0,0 +1,93 @@ +--- +title: Plugins +--- + +TypeScriptToLua supports plugins - an interface that allows to customize transpilation behavior. + +To add a plugin you have to add it under `tstl.luaPlugins` option in the [configuration file](../configuration.md). + +Example: + +```json title=tsconfig.json +{ + "tstl": { + "luaPlugins": [ + // Plugin is a JavaScript module exporting an object + { "name": "./plugin1.js" }, + // TypeScriptToLua can load plugins written in TypeScript using `ts-node` + { "name": "./plugin2.ts" }, + // Plugins can be published to npm + { "name": "tstl-plugin-3" } + ] + } +} +``` + +## API + +### `visitors` + +Internally, to process [Abstract Syntax Tree](https://basarat.gitbook.io/typescript/overview/ast) of a TypeScript program, TypeScriptToLua implements the [visitor pattern](https://en.wikipedia.org/wiki/Visitor_pattern). Visitor is a function, called with a processed node and transformation context, and returning a Lua AST node. Plugins can inject their own visitors using `visitors` property, overriding standard transformation behavior. + +Example: + +```ts +import * as ts from "typescript"; +import * as tstl from "typescript-to-lua"; + +const plugin: tstl.Plugin = { + // `visitors` is a record where keys are TypeScript node syntax kinds + visitors: { + // Visitor can be a function that returns Lua AST node + [ts.SyntaxKind.ReturnStatement]: () => tstl.createReturnStatement([tstl.createBooleanLiteral(true)]), + }, +}; + +export default plugin; +``` + +Example 2: + +```ts +import * as ts from "typescript"; +import * as tstl from "typescript-to-lua"; + +const plugin: tstl.Plugin = { + visitors: { + // Visit string literals, if original transformer returns a string literal, change the string to "bar" instead + [ts.SyntaxKind.StringLiteral]: (node, context) => { + // `context` exposes `superTransform*` methods, that can be used to call either the visitor provided by previous + // plugin, or a standard TypeScriptToLua visitor + const result = context.superTransformExpression(node); + + // Standard visitor for ts.StringLiteral always returns tstl.StringLiteral node + if (tstl.isStringLiteral(result)) { + result.value = "bar"; + } + + return result; + }, + }, +}; + +export default plugin; +``` + +### `printer` + +Printer is a function that overrides standard implementation of Lua AST printer. It receives some information about the file and transformed Lua AST. See [Printer](printer.md) page for more information. + +Example: + +```ts +import * as tstl from "typescript-to-lua"; + +class CustomLuaPrinter extends tstl.LuaPrinter {} + +const plugin: tstl.Plugin = { + printer: (program, emitHost, fileName, block, luaLibFeatures) => + new CustomLuaPrinter(program.getCompilerOptions(), emitHost, fileName).print(block, luaLibFeatures), +}; + +export default plugin; +``` diff --git a/docs/api/printer.md b/docs/api/printer.md new file mode 100644 index 00000000..f6c4ee65 --- /dev/null +++ b/docs/api/printer.md @@ -0,0 +1,61 @@ +--- +title: Printer +--- + +The [LuaPrinter](https://github.com/TypeScriptToLua/TypeScriptToLua/blob/master/src/LuaPrinter.ts) class takes Lua AST and prints it to a string (with source map). The printer implements the [visitor pattern](https://en.wikipedia.org/wiki/Visitor_pattern). All methods visit nodes in the AST to print them to a [`SourceNode`](https://github.com/mozilla/source-map#sourcenode), this will automatically produce correct mappings in the resulting source map. + +## API Reference + +```ts +interface PrintResult { + code: string; + sourceMap: string; + sourceMapNode: SourceNode; +} + +class LuaPrinter { + constructor(options: CompilerOptions, emitHost: EmitHost, fileName: string); + public print(block: lua.Block, luaLibFeatures: Set): PrintResult; + public printStatement(statement: lua.Statement): SourceNode; + public printDoStatement(statement: lua.DoStatement): SourceNode; + public printVariableDeclarationStatement(statement: lua.VariableDeclarationStatement): SourceNode; + public printVariableAssignmentStatement(statement: lua.AssignmentStatement): SourceNode; + public printIfStatement(statement: lua.IfStatement, isElseIf?: boolean): SourceNode; + public printWhileStatement(statement: lua.WhileStatement): SourceNode; + public printRepeatStatement(statement: lua.RepeatStatement): SourceNode; + public printForStatement(statement: lua.ForStatement): SourceNode; + public printForInStatement(statement: lua.ForInStatement): SourceNode; + public printGotoStatement(statement: lua.GotoStatement): SourceNode; + public printLabelStatement(statement: lua.LabelStatement): SourceNode; + public printReturnStatement(statement: lua.ReturnStatement): SourceNode; + public printBreakStatement(statement: lua.BreakStatement): SourceNode; + public printExpressionStatement(statement: lua.ExpressionStatement): SourceNode; + public printExpression(expression: lua.Expression): SourceNode; + public printStringLiteral(expression: lua.StringLiteral): SourceNode; + public printNumericLiteral(expression: lua.NumericLiteral): SourceNode; + public printNilLiteral(expression: lua.NilLiteral): SourceNode; + public printDotsLiteral(expression: lua.DotsLiteral): SourceNode; + public printBooleanLiteral(expression: lua.BooleanLiteral): SourceNode; + public printFunctionExpression(expression: lua.FunctionExpression): SourceNode; + public printFunctionDefinition(statement: lua.FunctionDefinition): SourceNode; + public printTableFieldExpression(expression: lua.TableFieldExpression): SourceNode; + public printTableExpression(expression: lua.TableExpression): SourceNode; + public printUnaryExpression(expression: lua.UnaryExpression): SourceNode; + public printBinaryExpression(expression: lua.BinaryExpression): SourceNode; + public printCallExpression(expression: lua.CallExpression): SourceNode; + public printMethodCallExpression(expression: lua.MethodCallExpression): SourceNode; + public printIdentifier(expression: lua.Identifier): SourceNode; + public printTableIndexExpression(expression: lua.TableIndexExpression): SourceNode; + public printOperator(kind: lua.Operator): SourceNode; + protected pushIndent(): void; + protected popIndent(): void; + protected indent(input?: SourceChunk): SourceChunk; + protected createSourceNode(node: lua.Node, chunks: SourceChunk | SourceChunk[], name?: string): SourceNode; + protected concatNodes(...chunks: SourceChunk[]): SourceNode; + protected printBlock(block: lua.Block): SourceNode; + protected printStatementArray(statements: lua.Statement[]): SourceChunk[]; + protected isStatementEmpty(statement: lua.Statement): boolean; + protected joinChunks(separator: string, chunks: SourceChunk[]): SourceChunk[]; + protected printExpressionList(expressions: lua.Expression[]): SourceChunk[]; +} +``` diff --git a/docs/caveats.md b/docs/caveats.md new file mode 100644 index 00000000..b33286ee --- /dev/null +++ b/docs/caveats.md @@ -0,0 +1,99 @@ +--- +title: Caveats +--- + +## Feature support + +| Feature | Lua 5.1 | Lua 5.2 | Lua 5.3 | LuaJIT | +| --------------------- | :-----: | :-----: | :-----: | :----: | +| (Everything else) | ✔️ | ✔️ | ✔️ | ✔️ | +| [Bitwise operators] | ❌ | ✔️ | ✔️ | ✔️ | +| [Switch statement] | ❌ | ✔️ | ✔️ | ✔️ | +| [`continue`] | ❌ | ✔️ | ✔️ | ✔️ | +| [`Promise`] | ❌ | ❌ | ❌ | ❌ | +| [`async`] / [`await`] | ❌ | ❌ | ❌ | ❌ | +| [Regular Expressions] | ❌ | ❌ | ❌ | ❌ | +| [Optional Chaining] | ❌ | ❌ | ❌ | ❌ | +| [Nullish Coalescing] | ❌ | ❌ | ❌ | ❌ | +| [Private Fields] | ❌ | ❌ | ❌ | ❌ | +| [JSX] | ❌ | ❌ | ❌ | ❌ | + +[bitwise operators]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Bitwise_Operators +[switch statement]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/switch +[`continue`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/continue +[`promise`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise +[`async`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function +[`await`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await +[regular expressions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions +[optional chaining]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining +[nullish coalescing]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator +[private fields]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Class_fields#Private_fields +[jsx]: https://www.typescriptlang.org/docs/handbook/jsx.html + +## Differences from JavaScript + +This project aims for both compilation results to have the same behavior as much as possible, but not at all costs. Since TypeScript is based on JavaScript it also inherited some of the quirks in JavaScript that are not present in Lua. This is where behavior between Lua and JavaScript compilation targets diverge. TypeScriptToLua aims to keep identical behavior as long as **sane** TypeScript is used: if JavaScript-specific quirks are used behavior might differ. + +Below are some of the cases where resulting Lua intentionally behaves different from compiled JS. + +### Type-directed emit + +One of TypeScript's [design goals](https://github.com/microsoft/TypeScript/wiki/TypeScript-Design-Goals) is **not** using type information to affect program runtime behavior. Though this has many advantages (such as gradual typing), TypeScriptToLua uses type information extensively. This allows us to emit a much more optimized, portable, and correct Lua code. + +### [Boolean coercion](https://developer.mozilla.org/en-US/docs/Glossary/Falsy) + +JavaScript and Lua differ in what they evaluate to true/false. TypeScriptToLua adheres to the Lua evaluations. + +| TypeScript | _JavaScript behavior_ | _Lua behavior_ | +| ----------------- | --------------------- | -------------- | +| `false` | `false` | `false` | +| `undefined` | `false` | `false` | +| `null` | `false` | `false` | +| `NaN` | `false` | ⚠️`true` | +| `""` | `false` | ⚠️`true` | +| `0` | `false` | ⚠️`true` | +| (Everything else) | `true` | `true` | + +### [Loose equality](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#Loose_equality_using) + +TypeScriptToLua makes no difference between `==` and `===` when compiling to Lua, treating all comparisons as strict (`===`). + +### Array Length + +`Array.prototype.length` is translated to Lua's `#` operator. Due to the way lists are implemented in Lua there can be differences between JavaScript's `list.length` and Lua's `#list`. The transpiler does not do anything to remedy these differences, so when working with lists, the transpiled Lua will use the standard Lua conventions. Generally speaking, the situation where these differences occur happen when adding/removing items to a list in a hacky way, or when setting list items to `undefined`/`null`. + +**Examples:** + +**Safe (no difference):** + +```ts +const myList = [1, 2, 3]; +myList.push(4); +myList.pop(); +myList.splice(1, 1); +// myList.length == 2 +``` + +**Differences might occur:** + +```ts +const myList = [1, 2, 3]; +myList[1] = undefined; +// myList.length == 1 (3 in JavaScript) +``` + +```ts +const myList = [1, 2, 3]; +myList[4] = 5; +// myList.length == 3 (5 in JavaScript) +``` + +### Key Iteration Order + +Even though iterating over object keys with `for ... in` does not guarantee order in either JavaScript or Lua. Therefore, the iteration order in JavaScript is likely different from the order in Lua. + +**Note:** If a specific order is required, it is better to use ordered collections like lists instead. + +### Iterating an array with `for ... in` + +Not allowed. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 00000000..bac7f6fb --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,57 @@ +--- +title: Configuration +--- + +TypeScriptToLua uses the same configuration file as the vanilla TypeScript compiler, loading it from the `tsconfig.json` file using the same rules as `tsc`. + +## Custom options + +To customize transpilation behavior we add a new group of options to the `tsconfig.json` file. All of these options should be placed in a `tstl` object. + +```json title=tsconfig.json +{ + "tstl": { + // custom options + } +} +``` + +| Option | Values | Description | +| -------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `luaTarget` | `"JIT"`, `"5.3"`, `"5.2"`, `"5.1"` (default: `"JIT"`) | Specifies the Lua version you want to generate code for. | +| `noImplicitSelf` | `true`, `false` (default: `false`) | If true, treats all project files as if they were prefixed with
`/** @noSelfInFile **/`. | +| `noHeader` | `true`, `false` (default: `false`) | Set this to true if you don't want to include our header in the output. | +| `luaLibImport` | `"inline"`, `"require"`, `"always"`, `"none"` (default: `"require"`) | We polyfill certain JavaScript features with Lua functions, this option specifies how these functions are imported into the Lua output. | +| `sourceMapTraceback` | `true`, `false` (default: `false`) | Overrides Lua's `debug.traceback` to apply sourcemaps to Lua stacktraces. This will make error messages point to your original TypeScript code instead of the generated Lua. | +| `luaBundle` | File path (relative to the `tsconfig.json`) | Will bundle all output lua files into a single bundle file. Requires **luaBundleEntry** to be set! | +| `luaBundleEntry` | File path (relative to the `tsconfig.json`) | This should be the name/path of the TS file in your project that will serve as entry point to the bundled code. | +| `luaPlugins` | `Array<{ name: string; import?: string }>` | List of [TypeScriptToLua plugins](plugins.md). | + +## Standard options + +Most of the standard [TypeScript options](https://www.typescriptlang.org/docs/handbook/compiler-options.html) work without any changes. Notable unsupported options are: + +- `composite`, `build` +- `incremental` +- `emitDecoratorMetadata` +- `esModuleInterop` + +Some options do not apply to TypeScriptToLua and are ignored: + +- `outFile` - use `luaBundle` instead. +- `importHelpers`, `noEmitHelpers` - use `luaLibImport` instead. +- `target`, `module` - it's only effect is limiting some features, so prefer to set it to `esnext`. If TypeScript requires you to specify different `module` type because you want to bundle your declarations with `outFile`, consider using [API Extractor](https://api-extractor.com/) instead. + +## Transformers + +Transformers is a powerful feature of TypeScript that allows you to modify behavior of your program during compilation. While TypeScript [currently](https://github.com/microsoft/TypeScript/issues/14419) does not provide a user-facing way to use transformers, TypeScriptToLua allows you to specify them in the configuration file, following [ttypescript](https://github.com/cevek/ttypescript#how-to-use) format. + +**Example:** + +```json title=tsconfig.json +{ + "compilerOptions": { + "plugins": [{ "transform": "dota-lua-types/transformer" }] + } +} +``` diff --git a/docs/editor-support.md b/docs/editor-support.md new file mode 100644 index 00000000..9ac7a781 --- /dev/null +++ b/docs/editor-support.md @@ -0,0 +1,46 @@ +--- +title: Editor Support +--- + +To have basic support for TypeScriptToLua it is enough to [configure your editor for TypeScript support](https://github.com/Microsoft/TypeScript/wiki/TypeScript-Editor-Support). + +## Language Service Plugin + +Sometimes TypeScriptToLua has to report it's own errors during the compilation. To have the same errors displayed in your editor, you can use a [language service plugin](https://github.com/microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin). + +![](/images/editor-support-diagnostics.png) + +To use it either get a [Visual Studio Code extension](https://marketplace.visualstudio.com/items?itemName=ark120202.vscode-typescript-to-lua) or [add it to your project](https://github.com/TypeScriptToLua/typescript-tstl-plugin#installation). + +## Build Tasks + +Most of advanced code editors can build your project with [npm scripts](https://docs.npmjs.com/misc/scripts). + +```json title=package.json +{ + "scripts": { + "build": "tstl", + "dev": "tstl --watch" + } +} +``` + +### Visual Studio Code + +VSCode supports running npm scripts using [tasks](https://code.visualstudio.com/docs/editor/tasks). To define a task, create a `.vscode/tasks.json` file or press `F1` and run `Tasks: Configure Task` command. Example configuration: + +```json title=tasks.json +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "dev", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { "reveal": "never" }, + "group": { "kind": "build", "isDefault": true } + } + ] +} +``` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..37ab0cae --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,81 @@ +--- +title: Getting Started +--- + +This is a quick introduction into project setup and our CLI. For a TypeScript quick start please read: https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html + +## Installation + +TypeScriptToLua is built using [Node.js](https://nodejs.org/) and distributed via [npm](https://www.npmjs.com/). To install it, you need to create a `package.json` file in the root of your project, containing at least `{}`. Then you can use this command to add the latest version of TypeScriptToLua to your project: + +```bash +npm install -D typescript-to-lua +``` + +:::note +Installing `tstl` locally is recommended to keep your build reproducible and prevent version conflicts between projects. However, it is also possible to install it globally with `npm install --global typescript-to-lua` or run it without install using `npx typescript-to-lua`. +::: + +## Project setup + +TypeScriptToLua shares the configuration format with vanilla TypeScript. This file is called `tsconfig.json` and should be located in your project's root. + +Basic recommended configuration: + +```json title=tsconfig.json +{ + "compilerOptions": { + "target": "esnext", + "lib": ["esnext"], + "types": [], + "strict": true + }, + "tstl": { + "luaTarget": "JIT" + } +} +``` + +Check out [Configuration](configuration.md) page for more information. + +## Building your project + +Our command line interface is called `tstl` and it works almost exactly as TypeScript's `tsc`. + +Since `tstl` is installed locally to your project, you cannot run it as a bare command in your terminal, so it's recommended to use it with [npm scripts](https://docs.npmjs.com/misc/scripts). + +```json title=package.json +{ + "private": true, + "scripts": { + "build": "tstl", + "dev": "tstl --watch" + }, + "devDependencies": { + "typescript-to-lua": "..." + } +} +``` + +```bash +# Build +npm run build + +# Build and watch for changes +npm run dev +``` + +:::note +For testing purposes you also can run `tstl` directly from your terminal with `node_modules/.bin/tstl` or `npx --no-install tstl`. +::: + +## Declarations + +The real power of this transpiler is usage together with good declarations for the Lua API provided. Some examples of Lua interface declarations can be found here: + +- [Lua Standard Library](https://github.com/TypeScriptToLua/lua-types) +- [Dota 2 Custom Games](https://github.com/ModDota/API/tree/master/declarations/server) ([template](https://github.com/ModDota/TypeScriptAddonTemplate)) +- [Defold Game Engine Scripting](https://github.com/dasannikov/DefoldTypeScript/blob/master/defold.d.ts) +- [LÖVE 2D Game Development](https://github.com/hazzard993/love-typescript-definitions) +- [World of Warcraft - Addon Development](https://github.com/wartoshika/wow-declarations) +- [World of Warcraft Classic - Addon Development](https://github.com/wartoshika/wow-classic-declarations) diff --git a/docusaurus.config.js b/docusaurus.config.js index 8cd48eb1..ca8b2235 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -11,6 +11,7 @@ module.exports = { title: "TypeScriptToLua", logo: { src: "images/logo.png" }, links: [ + { to: "docs/getting-started", label: "Docs", position: "left" }, { to: "play", label: "Playground", position: "left" }, { href: "https://discord.gg/BWAq58Y", label: "Discord", position: "right" }, { href: "https://github.com/TypeScriptToLua/TypeScriptToLua", label: "GitHub", position: "right" }, @@ -26,6 +27,10 @@ module.exports = { [ "@docusaurus/preset-classic", { + docs: { + sidebarPath: require.resolve("./sidebars.json"), + editUrl: "https://github.com/TypeScriptToLua/TypeScriptToLua.github.io/edit/source/", + }, theme: { customCss: require.resolve("./src/custom.scss"), }, diff --git a/sidebars.json b/sidebars.json new file mode 100644 index 00000000..4db4962d --- /dev/null +++ b/sidebars.json @@ -0,0 +1,22 @@ +{ + "docs": [ + "getting-started", + "configuration", + "caveats", + "editor-support", + { + "type": "category", + "label": "Advanced", + "items": [ + "advanced/writing-declarations", + "advanced/compiler-annotations", + "advanced/functions-and-the-self-parameter" + ] + }, + { + "type": "category", + "label": "API", + "items": ["api/overview", "api/plugins", "api/printer"] + } + ] +} diff --git a/src/components/SideBySide/index.tsx b/src/components/SideBySide/index.tsx new file mode 100644 index 00000000..31022be3 --- /dev/null +++ b/src/components/SideBySide/index.tsx @@ -0,0 +1,17 @@ +import React from "react"; +import styles from "./styles.module.scss"; + +export function SideBySide({ children }: { children: React.ReactNode }) { + const count = React.Children.count(children); + if (count !== 2) { + throw new Error(`Invalid SideBySide children count: ${count}`); + } + + const [left, right] = React.Children.toArray(children); + return ( +
+ {left} + {right} +
+ ); +} diff --git a/src/components/SideBySide/styles.module.scss b/src/components/SideBySide/styles.module.scss new file mode 100644 index 00000000..af02f2e9 --- /dev/null +++ b/src/components/SideBySide/styles.module.scss @@ -0,0 +1,22 @@ +.sideBySide { + display: flex; + + @media screen and (max-width: 996px) { + flex-flow: column; + } + + @media screen and (min-width: 997px) { + > :first-child { + padding-right: 5px; + } + + > :last-child { + padding-left: 5px; + } + } + + > div { + flex: 1 1 auto; + overflow: auto; + } +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 337d8856..53f14ae1 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -88,6 +88,12 @@ export default function Home() {

Write Lua with TypeScript

+ + Get Started + Try Online diff --git a/src/pages/play/code.ts b/src/pages/play/code.ts index 8cc4f740..b3ceff1f 100644 --- a/src/pages/play/code.ts +++ b/src/pages/play/code.ts @@ -39,3 +39,7 @@ export function updateCodeHistory(code: string) { const hash = `code/${lzstring.compressToEncodedURIComponent(code)}`; window.history.replaceState({}, "", `#${hash}`); } + +export function getPlaygroundUrlForCode(code: string) { + return `/play/#code/${lzstring.compressToEncodedURIComponent(code)}`; +} diff --git a/src/theme/CodeBlock/index.js b/src/theme/CodeBlock/index.js new file mode 100644 index 00000000..9ae78627 --- /dev/null +++ b/src/theme/CodeBlock/index.js @@ -0,0 +1,140 @@ +import Link from "@docusaurus/Link"; +import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; +import useThemeContext from "@theme/hooks/useThemeContext"; +import classnames from "classnames"; +import Clipboard from "clipboard"; +import rangeParser from "parse-numeric-range"; +import Highlight, { defaultProps } from "prism-react-renderer"; +import defaultTheme from "prism-react-renderer/themes/palenight"; +import React, { useEffect, useRef, useState } from "react"; +import { getPlaygroundUrlForCode } from "../../pages/play/code"; +import styles from "./styles.module.scss"; + +function useClipboard() { + const target = useRef(null); + const button = useRef(null); + + const [showCopied, setShowCopied] = useState(false); + useEffect(() => { + let clipboard; + + if (button.current) { + clipboard = new Clipboard(button.current, { + target: () => target.current, + }); + } + + return () => { + if (clipboard) { + clipboard.destroy(); + } + }; + }, [button.current, target.current]); + + const handleCopyCode = () => { + window.getSelection().empty(); + setShowCopied(true); + + setTimeout(() => setShowCopied(false), 2000); + }; + + return { showCopied, handleCopyCode, target, button }; +} + +function usePrismTheme(prism) { + const [mounted, setMounted] = useState(false); + // The Prism theme on SSR is always the default theme but the site theme + // can be in a different mode. React hydration doesn't update DOM styles + // that come from SSR. Hence force a re-render after mounting to apply the + // current relevant styles. There will be a flash seen of the original + // styles seen using this current approach but that's probably ok. Fixing + // the flash will require changing the theming approach and is not worth it + // at this point. + useEffect(() => { + setMounted(true); + }, []); + + const { isDarkTheme } = useThemeContext(); + const lightModeTheme = prism.theme || defaultTheme; + const darkModeTheme = prism.darkTheme || lightModeTheme; + const prismTheme = isDarkTheme ? darkModeTheme : lightModeTheme; + return { prismTheme, mounted }; +} + +export default ({ children, className: languageClassName, metastring = "" }) => { + const { + siteConfig: { + themeConfig: { prism = {} }, + }, + } = useDocusaurusContext(); + + const { prismTheme, mounted } = usePrismTheme(prism); + const { showCopied, handleCopyCode, target, button } = useClipboard(); + + const code = children.trim(); + const [, title] = metastring.match(/title=(.+)( |$)/) ?? []; + + const [, highlightLinesRange] = metastring.match(/{([\d,-]+)}/) ?? []; + const highlightLines = highlightLinesRange != null ? rangeParser.parse(highlightLinesRange).filter(n => n > 0) : []; + + let language = languageClassName && languageClassName.replace(/language-/, ""); + if (!language && prism.defaultLanguage) { + language = prism.defaultLanguage; + } + + const hasPlayground = language === "ts" || language === "typescript"; + + return ( + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( + <> + {title &&
{title}
} +
+                        
+
+                        {hasPlayground && (
+                            
+                                Playground
+                            
+                        )}
+
+                        
+                            {tokens.map((line, i) => {
+                                if (line.length === 1 && line[0].content === "") {
+                                    line[0].content = "\n";
+                                }
+
+                                const lineProps = getLineProps({ line, key: i });
+
+                                if (highlightLines.includes(i + 1)) {
+                                    lineProps.className = `${lineProps.className} docusaurus-highlight-code-line`;
+                                }
+
+                                return (
+                                    
+ {line.map((token, key) => ( + + ))} +
+ ); + })} +
+
+ + )} +
+ ); +}; diff --git a/src/theme/CodeBlock/styles.module.scss b/src/theme/CodeBlock/styles.module.scss new file mode 100644 index 00000000..9246ea7a --- /dev/null +++ b/src/theme/CodeBlock/styles.module.scss @@ -0,0 +1,54 @@ +.codeBlock { + overflow: auto; + display: block; + padding: 0; + margin: 0; + + &.hasTitle { + padding-top: 16px; + } +} + +.title { + position: absolute; + left: 8px; + padding: 3px 5px; + border-radius: 0 0 10% 10%; + background-color: var(--ifm-color-primary); + color: white; +} + +.codeBlockLines { + background-color: transparent; + border-radius: 0; + margin-bottom: 0; + float: left; + min-width: 100%; + padding: var(--ifm-pre-padding); +} + +.copyButton, +.playgroundButton { + background: rgb(1, 22, 39); + border: 1px solid rgb(214, 222, 235); + border-radius: var(--ifm-global-radius); + color: rgb(214, 222, 235); + cursor: pointer; + line-height: 12px; + opacity: 0; + outline: none; + padding: 4px 8px; + position: absolute; + right: var(--ifm-pre-padding); + top: var(--ifm-pre-padding); + visibility: hidden; + transition: opacity 200ms ease-in-out, visibility 200ms ease-in-out, bottom 200ms ease-in-out; + .codeBlock:hover > & { + visibility: visible; + opacity: 1; + } +} + +.playgroundButton { + top: calc(var(--ifm-pre-padding) + 28px); +} diff --git a/static/images/editor-support-diagnostics.png b/static/images/editor-support-diagnostics.png new file mode 100644 index 00000000..167e64c4 Binary files /dev/null and b/static/images/editor-support-diagnostics.png differ