Skip to content

Commit 9ea8811

Browse files
committed
✨ CLI & project building
1 parent 8ff5a48 commit 9ea8811

File tree

15 files changed

+474
-72
lines changed

15 files changed

+474
-72
lines changed

examples/TestProject/spark.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
name = "TestProject"
1+
name = "TestProject"
2+
3+
[dependencies]
4+
Spark = "https://github.com/MystPi/spark_stdlib/archive/refs/heads/main.tar.gz"

gleam.toml

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
name = "spark"
22
version = "1.0.0"
3+
target = "javascript"
34

45
# Fill out these fields if you intend to generate HTML documentation or publish
56
# your project to the Hex package manager.
@@ -12,16 +13,21 @@ version = "1.0.0"
1213
# For a full reference of all the available options, you can have a look at
1314
# https://gleam.run/writing-gleam/gleam-toml/.
1415

16+
[javascript]
17+
runtime = "bun"
18+
1519
[dependencies]
1620
gleam_stdlib = "~> 0.34 or ~> 1.0"
1721
chomp = { path = "../chomp-nibble" }
1822
gleam_community_ansi = ">= 1.4.0 and < 2.0.0"
19-
hug = "~> 1.0"
23+
hug = ">= 1.0.2 and < 2.0.0"
2024
glam = ">= 2.0.0 and < 3.0.0"
2125
dedent = ">= 1.0.0 and < 2.0.0"
2226
filepath = ">= 1.0.0 and < 2.0.0"
2327
simplifile = ">= 1.7.0 and < 2.0.0"
2428
tom = ">= 0.3.0 and < 1.0.0"
29+
glint = ">= 0.18.1 and < 1.0.0"
30+
argv = ">= 1.0.2 and < 2.0.0"
2531

2632
[dev-dependencies]
2733
gleeunit = "~> 1.0"

manifest.toml

+10-5
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,36 @@
22
# You typically do not need to edit this file
33

44
packages = [
5+
{ name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
56
{ name = "chomp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], source = "local", path = "../chomp-nibble" },
67
{ name = "dedent", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "dedent", source = "hex", outer_checksum = "591C78F019CFE8B4F138BDCA5DB57EB429D95F90CD12B51262A3FC6455EC1AEF" },
78
{ name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" },
89
{ name = "glam", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glam", source = "hex", outer_checksum = "66EC3BCD632E51EED029678F8DF419659C1E57B1A93D874C5131FE220DFAD2B2" },
910
{ name = "gleam_community_ansi", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "FE79E08BF97009729259B6357EC058315B6FBB916FAD1C2FF9355115FEB0D3A4" },
1011
{ name = "gleam_community_colour", version = "1.4.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "795964217EBEDB3DA656F5EB8F67D7AD22872EB95182042D3E7AFEF32D3FD2FE" },
1112
{ name = "gleam_json", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "9063D14D25406326C0255BDA0021541E797D8A7A12573D849462CAFED459F6EB" },
12-
{ name = "gleam_stdlib", version = "0.34.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1FB8454D2991E9B4C0C804544D8A9AD0F6184725E20D63C3155F0AEB4230B016" },
13+
{ name = "gleam_stdlib", version = "0.37.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "5398BD6C2ABA17338F676F42F404B9B7BABE1C8DC7380031ACB05BBE1BCF3742" },
1314
{ name = "gleeunit", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "72CDC3D3F719478F26C4E2C5FED3E657AC81EC14A47D2D2DEBB8693CA3220C3B" },
14-
{ name = "hug", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib"], otp_app = "hug", source = "hex", outer_checksum = "D7D8C4F5A3093FB0B6A0228288D94E95966AADC5500133F9E983BA4634E92CC8" },
15-
{ name = "pprint", version = "1.0.2", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "3EB2A7A8F72322F73EEF342374D0354AE39E7F9C64678B960BE8B2DC1B564AE1" },
15+
{ name = "glint", version = "0.18.1", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "5FB54D7732B4105E4AF4D89A7EE6D5E8CF33DA13A3575D0C6ECE470B97958454" },
16+
{ name = "hug", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib"], otp_app = "hug", source = "hex", outer_checksum = "0FB00C4F2E35155900BA90FF6240C63BD50C11037894836CD6AE04636A736C27" },
17+
{ name = "pprint", version = "1.0.3", build_tools = ["gleam"], requirements = ["glam", "gleam_stdlib"], otp_app = "pprint", source = "hex", outer_checksum = "76BBB92E23D12D954BD452686543F29EDE8EBEBB7FC0ACCBCA66EEF276EC3A06" },
1618
{ name = "simplifile", version = "1.7.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "1D5DFA3A2F9319EC85825F6ED88B8E449F381B0D55A62F5E61424E748E7DDEB0" },
19+
{ name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
1720
{ name = "thoas", version = "1.2.0", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "540C8CB7D9257F2AD0A14145DC23560F91ACDCA995F0CCBA779EB33AF5D859D1" },
1821
{ name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
1922
]
2023

2124
[requirements]
25+
argv = { version = ">= 1.0.2 and < 2.0.0"}
2226
chomp = { path = "../chomp-nibble" }
2327
dedent = { version = ">= 1.0.0 and < 2.0.0" }
2428
filepath = { version = ">= 1.0.0 and < 2.0.0" }
2529
glam = { version = ">= 2.0.0 and < 3.0.0" }
2630
gleam_community_ansi = { version = ">= 1.4.0 and < 2.0.0" }
2731
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
2832
gleeunit = { version = "~> 1.0" }
29-
hug = { version = "~> 1.0" }
33+
glint = { version = ">= 0.18.1 and < 1.0.0" }
34+
hug = { version = ">= 1.0.2 and < 2.0.0" }
3035
pprint = { version = "~> 1.0" }
3136
simplifile = { version = ">= 1.7.0 and < 2.0.0" }
32-
tom = { version = ">= 0.3.0 and < 1.0.0"}
37+
tom = { version = ">= 0.3.0 and < 1.0.0" }

src/ffi.mjs

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import * as $gleam from './gleam.mjs';
2+
import * as $error from './spark/error.mjs';
3+
4+
export function downloadTar(url, outputDir) {
5+
const wget_command = Bun.spawnSync(['wget', '-q', '-O', '-', url]);
6+
7+
if (!wget_command.success)
8+
return new $gleam.Error(
9+
$error.simple_error('Failed to download tarball from ' + url)
10+
);
11+
12+
const tar_command = Bun.spawnSync(
13+
['tar', 'xzC', outputDir, '--strip-components=1'],
14+
{
15+
stdin: wget_command.stdout,
16+
}
17+
);
18+
19+
if (!tar_command.success)
20+
return new $gleam.Error(
21+
$error.simple_error('Failed to extract tarball to ' + outputDir)
22+
);
23+
24+
return new $gleam.Ok();
25+
}
26+
27+
export function now() {
28+
return performance.now();
29+
}

src/spark.gleam

+138-27
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,158 @@
1+
import argv
12
import filepath
3+
import gleam/dict
24
import gleam/io
3-
import gleam/result.{try}
4-
import gleam/string
5-
import gleam_community/ansi
5+
import gleam/list
6+
import gleam/result
7+
import glint
8+
import glint/flag
9+
import spark/build
10+
import spark/error
611
import spark/file
712
import spark/project
13+
import spark/util
814

915
pub fn main() {
10-
let result = {
11-
use project <- try(project.from("examples/TestProject"))
12-
build(project)
13-
}
16+
glint.new()
17+
|> glint.with_name("spark")
18+
|> glint.with_pretty_help(glint.default_pretty_help())
19+
|> glint.add(at: ["build"], do: build_command())
20+
|> glint.add(at: ["clean"], do: clean_command())
21+
|> glint.add(at: ["new"], do: new_project_command())
22+
|> glint.run_and_handle(argv.load().arguments, print_result)
23+
}
1424

25+
fn print_result(result: Result(Nil, String)) -> Nil {
1526
case result {
1627
Ok(_) -> Nil
1728
Error(e) -> io.println_error(e)
1829
}
1930
}
2031

21-
/// Build a project, with the stdlib and prelude included, to the /build directory.
22-
///
23-
fn build(project: project.Project) -> Result(Nil, String) {
24-
let build_dir = filepath.join(project.dir, "build")
32+
// ---- FLAGS ------------------------------------------------------------------
33+
34+
const root = "root"
35+
36+
fn root_flag() -> flag.FlagBuilder(String) {
37+
flag.string()
38+
|> flag.default("./")
39+
|> flag.description("Change project root directory; default is ./")
40+
}
41+
42+
fn get_root_flag(input: glint.CommandInput) -> String {
43+
let assert Ok(root) = flag.get_string(input.flags, root)
44+
root
45+
}
46+
47+
// ---- COMMANDS ---------------------------------------------------------------
2548

26-
report("Compiling", "Spark")
27-
use template <- try(project.from("./templates"))
28-
use _ <- try(project.compile(template, to: build_dir))
49+
// -- Build --
2950

30-
report("Compiling", project.config.name)
31-
use _ <- try(project.compile(project, to: build_dir))
51+
fn build_command() {
52+
glint.command(fn(input) {
53+
input
54+
|> get_root_flag
55+
|> build
56+
})
57+
|> glint.description("Build a project")
58+
|> glint.flag(root, root_flag())
59+
|> glint.unnamed_args(glint.EqArgs(0))
60+
}
61+
62+
fn build(root: String) {
63+
project.from(root)
64+
|> result.then(build.build)
65+
}
66+
67+
// -- Clean --
68+
69+
fn clean_command() {
70+
glint.command(fn(input) {
71+
input
72+
|> get_root_flag
73+
|> clean
74+
})
75+
|> glint.description("Delete a project's build directory")
76+
|> glint.flag(root, root_flag())
77+
|> glint.unnamed_args(glint.EqArgs(0))
78+
}
79+
80+
fn clean(root: String) {
81+
filepath.join(root, "build")
82+
|> file.delete
83+
}
3284

33-
report("Creating", "index file")
34-
let path = filepath.join(project.dir, "build/index.mjs")
35-
let contents =
36-
"import { main } from './" <> project.config.name <> ".mjs';\nmain();"
37-
use _ <- try(file.write_all(path, contents))
85+
// -- New Project --
3886

39-
report("Compiled", "successfully")
40-
report("Entry", path)
41-
Ok(Nil)
87+
fn new_project_command() {
88+
glint.command(fn(input) {
89+
let assert Ok(name) = dict.get(input.named_args, "name")
90+
case util.is_valid_module_name(name) {
91+
True -> new_project(name)
92+
False ->
93+
Error(error.simple_error(
94+
"`"
95+
<> name
96+
<> "` is not a valid module name. Module names start with an uppercase letter and contain only letters and numbers",
97+
))
98+
}
99+
})
100+
|> glint.description("Create a new spark project")
101+
|> glint.named_args(["name"])
102+
|> glint.unnamed_args(glint.EqArgs(0))
42103
}
43104

44-
fn report(status: String, message: String) {
45-
let padding = string.repeat(" ", 9 - string.length(status))
46-
io.println(padding <> ansi.bold(ansi.blue(status)) <> " " <> message)
105+
fn new_project(name: String) {
106+
let files = [
107+
#(".gitignore", "/build\n"),
108+
#("README.md", new_project_readme(name)),
109+
#("spark.toml", new_project_config(name)),
110+
#("src/" <> name <> ".spark", new_project_code(name)),
111+
]
112+
113+
use _ <- result.try(
114+
list.try_each(files, fn(file) {
115+
let #(path, contents) = file
116+
file.safe_write_all(filepath.join(name, path), contents)
117+
}),
118+
)
119+
120+
Ok(io.println("Created project in ./" <> name))
121+
}
122+
123+
fn new_project_readme(name: String) -> String {
124+
"# " <> name <> "
125+
126+
A spark project.
127+
128+
```sh
129+
# build
130+
spark build
131+
132+
# run with Bun
133+
bun run ./build/index.mjs
134+
135+
# run with Deno
136+
deno run ./build/index.mjs
137+
138+
# run with Node
139+
node ./build/index.mjs
140+
```
141+
"
142+
}
143+
144+
fn new_project_config(name: String) -> String {
145+
"name = \"" <> name <> "\"
146+
147+
[dependencies]
148+
Spark = \"https://github.com/MystPi/spark_stdlib/archive/refs/heads/main.tar.gz\"
149+
"
150+
}
151+
152+
fn new_project_code(name: String) -> String {
153+
"import Spark/IO
154+
155+
def pub main =
156+
IO.println(\"Hello from " <> name <> "!\")
157+
"
47158
}

src/spark/build.gleam

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import filepath
2+
import gleam/float
3+
import gleam/int
4+
import gleam/io
5+
import gleam/list
6+
import gleam/result.{try}
7+
import gleam/string
8+
import gleam_community/ansi
9+
import spark/file
10+
import spark/prelude
11+
import spark/project
12+
13+
/// Build a project with its dependencies to the build directory.
14+
///
15+
pub fn build(project: project.Project) -> Result(Nil, String) {
16+
let build_dir = filepath.join(project.dir, "build")
17+
18+
let start_time = now()
19+
use _ <- try(build_dependencies(project, to: build_dir))
20+
21+
report("Compiling", project.config.name)
22+
use _ <- try(project.compile(project, to: build_dir))
23+
24+
use path <- try(project.create_entrypoint(project, in: build_dir))
25+
use _ <- try(prelude.create(in: build_dir))
26+
let end_time = now()
27+
28+
report(
29+
"Compiled",
30+
"in " <> float.to_string(round(end_time -. start_time)) <> "ms",
31+
)
32+
report("Entry", path)
33+
Ok(Nil)
34+
}
35+
36+
@external(javascript, "../ffi.mjs", "now")
37+
fn now() -> Float
38+
39+
fn round(f: Float) -> Float {
40+
int.to_float(float.round(f *. 100.0)) /. 100.0
41+
}
42+
43+
fn build_dependencies(
44+
project: project.Project,
45+
to build_dir: String,
46+
) -> Result(Nil, String) {
47+
list.try_each(project.config.dependencies, fn(dep) {
48+
let #(name, url) = dep
49+
let path = filepath.join(build_dir, "deps/" <> name)
50+
51+
use _ <- try({
52+
case file.directory_exists(path) {
53+
True -> Ok(Nil)
54+
False -> {
55+
report("Downloading", name)
56+
download_dependency(from: url, to: path)
57+
}
58+
}
59+
})
60+
61+
report("Compiling", name)
62+
use dep_project <- try(project.from(path))
63+
use _ <- try(project.compile(dep_project, to: build_dir))
64+
65+
build_dependencies(dep_project, to: build_dir)
66+
})
67+
}
68+
69+
@external(javascript, "../ffi.mjs", "downloadTar")
70+
fn download_tar(from url: String, to dir: String) -> Result(Nil, String)
71+
72+
fn download_dependency(from url: String, to dir: String) -> Result(Nil, String) {
73+
use _ <- try(file.create_directory_all(dir))
74+
download_tar(from: url, to: dir)
75+
}
76+
77+
fn report(status: String, message: String) {
78+
let padding = string.repeat(" ", 11 - string.length(status))
79+
io.println(
80+
padding <> ansi.blue(status) <> ansi.dim(": ") <> ansi.bold(message),
81+
)
82+
}

src/spark/cli.gleam

-1
This file was deleted.

0 commit comments

Comments
 (0)