diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 580b94dd2f6..ab8ccb6ba42 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -470,6 +470,10 @@ jobs: run: make working-directory: ./test/javascript_prelude + - name: Test generated TypeScript declarations + run: make test + working-directory: ./test/typescript_declarations + - name: Test export of hex tarball run: make test working-directory: ./test/hextarball diff --git a/Makefile b/Makefile index 228545d0864..8d6ea998163 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ test: ## Run the compiler unit tests cd test/project_javascript && cargo run clean && cargo run check && cargo run test cd test/project_deno && cargo run clean && cargo run check && cargo run test cd test/hextarball && make test + cd test/typescript_declarations && make test cd test/running_modules && make test cd test/subdir_ffi && make @@ -51,6 +52,10 @@ test-watch: ## Run compiler tests when files change export-hex-tarball-test: ## Run `gleam export hex-tarball` and verify it is created cd test/hextarball && make test +.PHONY: typescript-declarations-test +typescript-declarations-test: ## Check that generated TypeScript declaration compile + cd test/typescript_declarations && make test + .PHONY: benchmark benchmark: ## Run the benchmarks cd benchmark/list && make diff --git a/test/typescript_declarations/.gitignore b/test/typescript_declarations/.gitignore new file mode 100644 index 00000000000..599be4eb929 --- /dev/null +++ b/test/typescript_declarations/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/test/typescript_declarations/Makefile b/test/typescript_declarations/Makefile new file mode 100644 index 00000000000..4452639f0da --- /dev/null +++ b/test/typescript_declarations/Makefile @@ -0,0 +1,8 @@ +.PHONY: clean +clean: + rm -rf build + +.PHONY: test +test: + cargo run --quiet -- build + tsc ./main.ts --strict --noEmit --lib es2015,dom diff --git a/test/typescript_declarations/README.md b/test/typescript_declarations/README.md new file mode 100644 index 00000000000..012d33f6529 --- /dev/null +++ b/test/typescript_declarations/README.md @@ -0,0 +1,3 @@ +# typescript_declarations + +Check that generated TypeScript declarartions are correct. This requires `tsc` installed and be in PATH. diff --git a/test/typescript_declarations/gleam.toml b/test/typescript_declarations/gleam.toml new file mode 100644 index 00000000000..fc53a2d88c1 --- /dev/null +++ b/test/typescript_declarations/gleam.toml @@ -0,0 +1,9 @@ +name = "typescript_declarations" +version = "1.0.0" +target = "javascript" + +[javascript] +typescript_declarations = true + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" diff --git a/test/typescript_declarations/main.ts b/test/typescript_declarations/main.ts new file mode 100644 index 00000000000..8b7ba85c4e1 --- /dev/null +++ b/test/typescript_declarations/main.ts @@ -0,0 +1,79 @@ +import type { Option$ } from "./build/dev/javascript/gleam_stdlib/gleam/option.d.mts"; +import { List, Ok, Result } from "./build/dev/javascript/typescript_declarations/gleam.mjs"; +import * as Gleam from "./build/dev/javascript/typescript_declarations/typescript_declarations.mjs" + +// Check add and add_alias +const sum: number = Gleam.add_alias(Gleam.add(1, 2), 908); + +// Check ID lists constants +const count_of_moderators: number = Gleam.moders.countLength(); +const count_of_administrators: number = Gleam.admins.countLength(); + +// Check add_one closure +const add_one: (_: number) => number = Gleam.add_one() +const two: number = add_one(1) + +// Check UserId type alias +const first_moderator: Gleam.UserId = Gleam.moders.toArray()[ 0]; +const first_moderator_num: number = first_moderator; +const me: UserID = 84738; +type UserID = Gleam.UserId; + +// Check is_divisible_by +const is_five_divisible_by_two: boolean = Gleam.is_divisible_by(5, 2); + +// Check extern alert function if we are in browser target +if (typeof window != undefined) { + Gleam.js_alert("Hello!"); +} + +// Check generic twice function +const twice_number: number = Gleam.twice(45, add_one); + +// Check recursive sum_list +const sum_of_list: number = Gleam.sum_list(List.fromArray([10, 25, 65383, 8910, 1893]), 0); + +// Check results +const result_ok: Result = new Ok(10); +const is_ok: boolean = Gleam.is_ok_result(result_ok); + +// Check name_description tuple +const name: string = Gleam.name_description[0]; +const description: string = Gleam.name_description[1]; + +// Check User and related functions +const user: Gleam.User$ = new Gleam.User("King", 83874, new Gleam.PlainUser()); +const guest: Gleam.User$ = new Gleam.Guest(); +const user_username: Option$ = Gleam.user_name(user); +const guest_username: Option$ = Gleam.user_name(guest); +const plain_user_string: string = Gleam.role_string((user as Gleam.User).role); + +// Check Either type +const left_either: Gleam.Either$ = new Gleam.Left("Hello!"); +const right_either: Gleam.Either$ = new Gleam.Right(3747); + +// This added since TypeScript will give warnings about unused things otherwise +void [ + sum, + count_of_administrators, + count_of_moderators, + add_one, + two, + first_moderator, + first_moderator_num, + me, + is_five_divisible_by_two, + twice_number, + sum_of_list, + result_ok, + is_ok, + name, + description, + user, + guest, + user_username, + guest_username, + plain_user_string, + left_either, + right_either, +] diff --git a/test/typescript_declarations/manifest.toml b/test/typescript_declarations/manifest.toml new file mode 100644 index 00000000000..88377adc600 --- /dev/null +++ b/test/typescript_declarations/manifest.toml @@ -0,0 +1,9 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.62.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "0080706D3A5A9A36C40C68481D1D231D243AF602E6D2A2BE67BA8F8F4DFF45EC" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } diff --git a/test/typescript_declarations/src/typescript_declarations.gleam b/test/typescript_declarations/src/typescript_declarations.gleam new file mode 100644 index 00000000000..87b6ec07dbc --- /dev/null +++ b/test/typescript_declarations/src/typescript_declarations.gleam @@ -0,0 +1,102 @@ +import gleam/option.{type Option} +import gleam/result + +/// Greeting for user +pub const greeting: String = "Hello, dear user!" + +/// Add two numbers +pub fn add(first a: Int, second b: Int) -> Int { + a + b +} + +/// Returns true if number is divisible by another +pub fn is_divisible_by(number num: Int, divider div: Int) -> Bool { + case num % div { + 0 -> True + _ -> False + } +} + +/// ID of specific user +pub type UserId = + Int + +/// Alias to add function +pub const add_alias = add + +/// Return function that will add one to given number +pub fn add_one() -> fn(Int) -> Int { + let func = add(1, _) + func +} + +/// IDs of administrators +pub const admins: List(UserId) = [00_010, 00_000, 00_001, 00_002, 29_373] + +/// IDs of moderators +pub const moders = [01_013, 36_371, 74_839, 18_930, 36_373] + +/// Create alert on browser target +@external(javascript, "globalThis", "alert") +pub fn js_alert(text: String) -> Nil + +/// Twice value via given function +pub fn twice(val: a, function: fn(a) -> a) -> a { + function(function(val)) +} + +/// Recursively sum list of numbers +pub fn sum_list(list: List(Int), total: Int) -> Int { + case list { + [first, ..rest] -> sum_list(rest, total + first) + [] -> total + } +} + +/// Returns true if result is ok +pub fn is_ok_result(res: Result(a, b)) -> Bool { + result.is_ok(res) +} + +/// First value is name and second is description +pub const name_description = #("MyApp", "My Awesome Application") + +/// Role of user +pub type UserRole { + /// Administrator + Administrator + /// Moderator + Moderator + /// Just a user + PlainUser +} + +/// Represents user +pub type User { + /// Represents registered user + User(username: String, id: UserId, role: UserRole) + /// Represents guest without any data + Guest +} + +/// Get name of user, None if user is guest +pub fn user_name(user: User) -> Option(String) { + case user { + User(name, ..) -> option.Some(name) + Guest -> option.None + } +} + +/// Format user role as string +pub fn role_string(role: UserRole) -> String { + case role { + PlainUser -> "user" + Moderator -> "moderator" + Administrator -> "admin" + } +} + +pub type Either(a, b) { + Left(a) + Right(b) +}