+23
.github/workflows/test.yml
+23
.github/workflows/test.yml
···
1
+
name: test
2
+
3
+
on:
4
+
push:
5
+
branches:
6
+
- master
7
+
- main
8
+
pull_request:
9
+
10
+
jobs:
11
+
test:
12
+
runs-on: ubuntu-latest
13
+
steps:
14
+
- uses: actions/checkout@v3
15
+
- uses: erlef/setup-beam@v1
16
+
with:
17
+
otp-version: "26.0.2"
18
+
gleam-version: "1.0.0"
19
+
rebar3-version: "3"
20
+
# elixir-version: "1.15.4"
21
+
- run: gleam deps download
22
+
- run: gleam test
23
+
- run: gleam format --check src test
+25
README.md
+25
README.md
···
1
+
# lustre_dev_tools
2
+
3
+
[](https://hex.pm/packages/lustre_dev_tools)
4
+
[](https://hexdocs.pm/lustre_dev_tools/)
5
+
6
+
```sh
7
+
gleam add lustre_dev_tools
8
+
```
9
+
```gleam
10
+
import lustre_dev_tools
11
+
12
+
pub fn main() {
13
+
// TODO: An example of the project in use
14
+
}
15
+
```
16
+
17
+
Further documentation can be found at <https://hexdocs.pm/lustre_dev_tools>.
18
+
19
+
## Development
20
+
21
+
```sh
22
+
gleam run # Run the project
23
+
gleam test # Run the tests
24
+
gleam shell # Run an Erlang shell
25
+
```
+30
gleam.toml
+30
gleam.toml
···
1
+
name = "lustre_dev_tools"
2
+
version = "1.0.0"
3
+
4
+
description = "Lustre's official CLI and development tooling."
5
+
repository = { type = "github", user = "lustre-labs", repo = "dev-tools" }
6
+
licences = ["MIT"]
7
+
8
+
links = [
9
+
{ title = "Sponsor", href = "https://github.com/sponsors/hayleigh-dot-dev" },
10
+
]
11
+
12
+
internal_modules = ["lusre_dev_tools", "lusre_dev_tools/*"]
13
+
14
+
[dependencies]
15
+
gleam_stdlib = "~> 0.34 or ~> 1.0"
16
+
glint = "0.16.0"
17
+
spinner = "~> 1.1"
18
+
gleam_package_interface = "~> 1.0"
19
+
tom = "~> 0.3"
20
+
gleam_erlang = "~> 0.25"
21
+
gleam_otp = "~> 0.10"
22
+
fs = "~> 8.6"
23
+
gleam_community_ansi = "~> 1.4"
24
+
filepath = "~> 0.1"
25
+
simplifile = "~> 1.5"
26
+
gleam_json = "~> 1.0"
27
+
argv = "~> 1.0"
28
+
29
+
[dev-dependencies]
30
+
gleeunit = "~> 1.0"
+40
manifest.toml
+40
manifest.toml
···
1
+
# This file was generated by Gleam
2
+
# You typically do not need to edit this file
3
+
4
+
packages = [
5
+
{ name = "argv", version = "1.0.1", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "A6E9009E50BBE863EB37D963E4315398D41A3D87D0075480FC244125808F964A" },
6
+
{ name = "filepath", version = "0.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "FC1B1B29438A5BA6C990F8047A011430BEC0C5BA638BFAA62718C4EAEFE00435" },
7
+
{ name = "fs", version = "8.6.1", build_tools = ["rebar3"], requirements = [], otp_app = "fs", source = "hex", outer_checksum = "61EA2BDAEDAE4E2024D0D25C63E44DCCF65622D4402DB4A2DF12868D1546503F" },
8
+
{ 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" },
9
+
{ name = "gleam_community_colour", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "A49A5E3AE8B637A5ACBA80ECB9B1AFE89FD3D5351FF6410A42B84F666D40D7D5" },
10
+
{ name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" },
11
+
{ name = "gleam_json", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "thoas"], otp_app = "gleam_json", source = "hex", outer_checksum = "8B197DD5D578EA6AC2C0D4BDC634C71A5BCA8E7DB5F47091C263ECB411A60DF3" },
12
+
{ name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" },
13
+
{ name = "gleam_package_interface", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "52A721BCA972C8099BB881195D821AAA64B9F2655BECC102165D5A1097731F01" },
14
+
{ name = "gleam_stdlib", version = "0.36.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "C0D14D807FEC6F8A08A7C9EF8DFDE6AE5C10E40E21325B2B29365965D82EB3D4" },
15
+
{ name = "glearray", version = "0.2.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glearray", source = "hex", outer_checksum = "908154F695D330E06A37FAB2C04119E8F315D643206F8F32B6A6C14A8709FFF4" },
16
+
{ name = "gleeunit", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D364C87AFEB26BDB4FB8A5ABDE67D635DC9FA52D6AB68416044C35B096C6882D" },
17
+
{ name = "glint", version = "0.16.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_community_colour", "gleam_stdlib", "snag"], otp_app = "glint", source = "hex", outer_checksum = "61B7E85CBB0CCD2FD8A9C7AE06CA97A80BF6537716F34362A39DF9C74967BBBC" },
18
+
{ name = "repeatedly", version = "2.1.1", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "38808C3EC382B0CD981336D5879C24ECB37FCB9C1D1BD128F7A80B0F74404D79" },
19
+
{ name = "simplifile", version = "1.5.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "EB9AA8E65E5C1E3E0FDCFC81BC363FD433CB122D7D062750FFDF24DE4AC40116" },
20
+
{ name = "snag", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "54D32E16E33655346AA3E66CBA7E191DE0A8793D2C05284E3EFB90AD2CE92BCC" },
21
+
{ name = "spinner", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_community_ansi", "gleam_erlang", "gleam_stdlib", "glearray", "repeatedly"], otp_app = "spinner", source = "hex", outer_checksum = "200BA3D4A04D468898E63C0D316E23F526E02514BC46454091975CB5BAE41E8F" },
22
+
{ name = "thoas", version = "0.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "thoas", source = "hex", outer_checksum = "4918D50026C073C4AB1388437132C77A6F6F7C8AC43C60C13758CC0ADCE2134E" },
23
+
{ name = "tom", version = "0.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "tom", source = "hex", outer_checksum = "0831C73E45405A2153091226BF98FB485ED16376988602CC01A5FD086B82D577" },
24
+
]
25
+
26
+
[requirements]
27
+
argv = { version = "~> 1.0" }
28
+
filepath = { version = "~> 0.1" }
29
+
fs = { version = "~> 8.6"}
30
+
gleam_community_ansi = { version = "~> 1.4" }
31
+
gleam_erlang = { version = "~> 0.25" }
32
+
gleam_json = { version = "~> 1.0" }
33
+
gleam_otp = { version = "~> 0.10" }
34
+
gleam_package_interface = { version = "~> 1.0" }
35
+
gleam_stdlib = { version = "~> 0.34 or ~> 1.0" }
36
+
gleeunit = { version = "~> 1.0" }
37
+
glint = { version = "0.16.0" }
38
+
simplifile = { version = "~> 1.5" }
39
+
spinner = { version = "~> 1.1" }
40
+
tom = { version = "~> 0.3" }
+4
priv/component-entry.mjs
+4
priv/component-entry.mjs
+3
priv/entry-with-main.mjs
+3
priv/entry-with-main.mjs
+4
priv/entry-with-start.mjs
+4
priv/entry-with-start.mjs
+19
priv/index-with-lustre-ui.html
+19
priv/index-with-lustre-ui.html
···
1
+
<!doctype html>
2
+
<html lang="en">
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+
7
+
<title>🚧 {app_name}</title>
8
+
9
+
<link
10
+
rel="stylesheet"
11
+
href="https://cdn.jsdelivr.net/gh/lustre-labs/ui/priv/styles.css"
12
+
/>
13
+
<script type="module" src="./index.mjs"></script>
14
+
</head>
15
+
16
+
<body>
17
+
<div id="app"></div>
18
+
</body>
19
+
</html>
+15
priv/index.html
+15
priv/index.html
···
1
+
<!doctype html>
2
+
<html lang="en">
3
+
<head>
4
+
<meta charset="UTF-8" />
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+
7
+
<title>🚧 {app_name}</title>
8
+
9
+
<script type="module" src="./index.mjs"></script>
10
+
</head>
11
+
12
+
<body>
13
+
<div id="app"></div>
14
+
</body>
15
+
</html>
+7
priv/tailwind.config.js
+7
priv/tailwind.config.js
+23
src/lustre/dev.gleam
+23
src/lustre/dev.gleam
···
1
+
// IMPORTS ---------------------------------------------------------------------
2
+
3
+
import argv
4
+
import glint
5
+
import lustre_dev_tools/cli/add
6
+
import lustre_dev_tools/cli/start
7
+
8
+
// MAIN ------------------------------------------------------------------------
9
+
10
+
pub fn main() {
11
+
let args = argv.load().arguments
12
+
13
+
glint.new()
14
+
|> glint.as_gleam_module
15
+
|> glint.with_name("lustre/dev")
16
+
|> glint.with_pretty_help(glint.default_pretty_help())
17
+
|> glint.add(at: ["add", "esbuild"], do: add.esbuild())
18
+
|> glint.add(at: ["add", "tailwind"], do: add.tailwind())
19
+
// |> glint.add(at: ["build", "app"], do: build.app())
20
+
// |> glint.add(at: ["build", "component"], do: build.component())
21
+
|> glint.add(at: ["start"], do: start.run())
22
+
|> glint.run(args)
23
+
}
+265
src/lustre_dev_tools/cli.gleam
+265
src/lustre_dev_tools/cli.gleam
···
1
+
// IMPORTS ---------------------------------------------------------------------
2
+
3
+
import gleam_community/ansi
4
+
import gleam/erlang
5
+
import gleam/result
6
+
import gleam/io
7
+
import gleam/string
8
+
import simplifile
9
+
import spinner.{type Spinner}
10
+
11
+
// TYPES -----------------------------------------------------------------------
12
+
13
+
pub opaque type Env {
14
+
Env(spinner: SpinnerStatus)
15
+
}
16
+
17
+
type SpinnerStatus {
18
+
Waiting
19
+
Running(spinner: Spinner, message: String)
20
+
Paused(spinner: Spinner)
21
+
}
22
+
23
+
///
24
+
///
25
+
pub type Cli(state, a, e) {
26
+
Cli(run: fn(Env, state) -> #(Env, state, Result(a, e)))
27
+
}
28
+
29
+
//
30
+
31
+
///
32
+
///
33
+
pub fn run(step: Cli(state, a, e), with state: state) -> Result(a, e) {
34
+
let env = Env(spinner: Waiting)
35
+
let #(env, _, result) = step.run(env, state)
36
+
37
+
case env.spinner {
38
+
Waiting -> Nil
39
+
Running(spinner, _) -> spinner.stop(spinner)
40
+
Paused(spinner) -> spinner.stop(spinner)
41
+
}
42
+
43
+
result
44
+
}
45
+
46
+
//
47
+
48
+
///
49
+
///
50
+
pub fn return(value: a) -> Cli(state, a, e) {
51
+
use env, state <- Cli
52
+
53
+
#(env, state, Ok(value))
54
+
}
55
+
56
+
///
57
+
///
58
+
pub fn throw(error: e, message: String) -> Cli(state, a, e) {
59
+
use env, state <- Cli
60
+
61
+
case env.spinner {
62
+
Waiting -> #(env, state, Error(error))
63
+
Running(spinner, _) -> {
64
+
spinner.stop(spinner)
65
+
io.println("❌ " <> ansi.red(message))
66
+
67
+
#(Env(Paused(spinner)), state, Error(error))
68
+
}
69
+
Paused(spinner) -> {
70
+
io.println("❌ " <> ansi.red(message))
71
+
72
+
#(Env(Paused(spinner)), state, Error(error))
73
+
}
74
+
}
75
+
}
76
+
77
+
pub fn from_result(result: Result(a, e)) -> Cli(state, a, e) {
78
+
use env, state <- Cli
79
+
80
+
#(env, state, result)
81
+
}
82
+
83
+
//
84
+
85
+
///
86
+
///
87
+
pub fn do(
88
+
step: Cli(state, a, e),
89
+
then next: fn(a) -> Cli(state, b, e),
90
+
) -> Cli(state, b, e) {
91
+
use env, state <- Cli
92
+
let #(env, state, result) = step.run(env, state)
93
+
94
+
case result {
95
+
Ok(value) -> next(value).run(env, state)
96
+
Error(error) -> {
97
+
case env.spinner {
98
+
Waiting -> Nil
99
+
Paused(_) -> Nil
100
+
Running(spinner, message) -> {
101
+
spinner.stop(spinner)
102
+
io.println("❌ " <> ansi.red(message))
103
+
}
104
+
}
105
+
106
+
#(env, state, Error(error))
107
+
}
108
+
}
109
+
}
110
+
111
+
pub fn map(step: Cli(state, a, e), then next: fn(a) -> b) -> Cli(state, b, e) {
112
+
use env, state <- Cli
113
+
let #(env, state, result) = step.run(env, state)
114
+
let result = result.map(result, next)
115
+
116
+
#(env, state, result)
117
+
}
118
+
119
+
pub fn map_error(
120
+
step: Cli(state, a, e),
121
+
then next: fn(e) -> f,
122
+
) -> Cli(state, a, f) {
123
+
use env, state <- Cli
124
+
let #(env, state, result) = step.run(env, state)
125
+
let result = result.map_error(result, next)
126
+
127
+
#(env, state, result)
128
+
}
129
+
130
+
///
131
+
///
132
+
pub fn do_result(
133
+
result: Result(a, e),
134
+
then next: fn(a) -> Cli(state, b, e),
135
+
) -> Cli(state, b, e) {
136
+
use env, state <- Cli
137
+
138
+
case result {
139
+
Ok(a) -> next(a).run(env, state)
140
+
Error(e) -> #(env, state, Error(e))
141
+
}
142
+
}
143
+
144
+
///
145
+
///
146
+
pub fn try(
147
+
step: Result(a, x),
148
+
catch recover: fn(x) -> e,
149
+
then next: fn(a) -> Cli(state, b, e),
150
+
) -> Cli(state, b, e) {
151
+
use env, state <- Cli
152
+
153
+
case step {
154
+
Ok(value) -> next(value).run(env, state)
155
+
Error(error) -> {
156
+
case env.spinner {
157
+
Waiting -> Nil
158
+
Paused(_) -> Nil
159
+
Running(spinner, message) -> {
160
+
spinner.stop(spinner)
161
+
io.println("❌ " <> ansi.red(message))
162
+
}
163
+
}
164
+
165
+
#(env, state, Error(recover(error)))
166
+
}
167
+
}
168
+
}
169
+
170
+
//
171
+
172
+
///
173
+
///
174
+
pub fn log(
175
+
message: String,
176
+
then next: fn() -> Cli(state, a, e),
177
+
) -> Cli(state, a, e) {
178
+
use env, state <- Cli
179
+
let env =
180
+
Env(spinner: case env.spinner {
181
+
Waiting ->
182
+
Running(
183
+
spinner.new(message)
184
+
|> spinner.with_frames(spinner.snake_frames)
185
+
|> spinner.start,
186
+
message,
187
+
)
188
+
189
+
Running(spinner, _) -> {
190
+
spinner.set_text(spinner, message)
191
+
Running(spinner, message)
192
+
}
193
+
194
+
Paused(spinner) -> {
195
+
spinner.set_text(spinner, message)
196
+
Running(spinner, message)
197
+
}
198
+
})
199
+
200
+
next().run(env, state)
201
+
}
202
+
203
+
pub fn success(
204
+
message: String,
205
+
then next: fn() -> Cli(state, a, e),
206
+
) -> Cli(state, a, e) {
207
+
use env, state <- Cli
208
+
let env =
209
+
Env(spinner: case env.spinner {
210
+
Waiting -> Waiting
211
+
Paused(spinner) -> Paused(spinner)
212
+
Running(spinner, _) -> {
213
+
spinner.stop(spinner)
214
+
Paused(spinner)
215
+
}
216
+
})
217
+
218
+
io.println("✅ " <> ansi.green(message))
219
+
next().run(env, state)
220
+
}
221
+
222
+
//
223
+
224
+
///
225
+
///
226
+
pub fn get_state() -> Cli(state, state, e) {
227
+
use env, state <- Cli
228
+
229
+
#(env, state, Ok(state))
230
+
}
231
+
232
+
///
233
+
///
234
+
pub fn set_state(value: state) -> Cli(state, Nil, e) {
235
+
use env, _ <- Cli
236
+
237
+
#(env, value, Ok(Nil))
238
+
}
239
+
240
+
//
241
+
242
+
@external(erlang, "lustre_dev_tools_ffi", "exec")
243
+
pub fn exec(
244
+
run command: String,
245
+
with args: List(String),
246
+
in in: String,
247
+
) -> Result(String, #(Int, String))
248
+
249
+
pub fn template(
250
+
name: String,
251
+
on_error: fn(String) -> e,
252
+
then next: fn(String) -> Cli(state, a, e),
253
+
) -> Cli(state, a, e) {
254
+
use env, state <- Cli
255
+
let assert Ok(priv) = erlang.priv_directory("lustre_dev_tools")
256
+
257
+
case simplifile.read(priv <> "/" <> name) {
258
+
Ok(template) -> next(template).run(env, state)
259
+
Error(error) -> #(
260
+
env,
261
+
state,
262
+
Error(on_error(name <> ": " <> string.inspect(error))),
263
+
)
264
+
}
265
+
}
+92
src/lustre_dev_tools/cli/add.gleam
+92
src/lustre_dev_tools/cli/add.gleam
···
1
+
// IMPORTS ---------------------------------------------------------------------
2
+
3
+
import glint.{type Command, CommandInput}
4
+
import glint/flag
5
+
import lustre_dev_tools/esbuild
6
+
import lustre_dev_tools/cli
7
+
import lustre_dev_tools/tailwind
8
+
9
+
// COMMANDS --------------------------------------------------------------------
10
+
11
+
pub fn esbuild() -> Command(Nil) {
12
+
let description =
13
+
"
14
+
Download a platform-appropriate version of the esbuild binary. Lustre uses this
15
+
to bundle applications and act as a development server.
16
+
"
17
+
18
+
glint.command(fn(input) {
19
+
let CommandInput(flags: flags, ..) = input
20
+
let assert Ok(os) = flag.get_string(flags, "os")
21
+
let assert Ok(cpu) = flag.get_string(flags, "cpu")
22
+
let script = esbuild.download(os, cpu)
23
+
24
+
case cli.run(script, Nil) {
25
+
Ok(_) -> Nil
26
+
Error(error) -> esbuild.explain(error)
27
+
}
28
+
})
29
+
|> glint.description(description)
30
+
|> glint.unnamed_args(glint.EqArgs(0))
31
+
|> glint.flag("os", {
32
+
let description = ""
33
+
let default = get_os()
34
+
35
+
flag.string()
36
+
|> flag.default(default)
37
+
|> flag.description(description)
38
+
})
39
+
|> glint.flag("cpu", {
40
+
let description = ""
41
+
let default = get_cpu()
42
+
43
+
flag.string()
44
+
|> flag.default(default)
45
+
|> flag.description(description)
46
+
})
47
+
}
48
+
49
+
pub fn tailwind() -> Command(Nil) {
50
+
let description =
51
+
"
52
+
Download a platform-appropriate version of the Tailwind binary.
53
+
"
54
+
55
+
glint.command(fn(input) {
56
+
let CommandInput(flags: flags, ..) = input
57
+
let assert Ok(os) = flag.get_string(flags, "os")
58
+
let assert Ok(cpu) = flag.get_string(flags, "cpu")
59
+
let script = tailwind.setup(os, cpu)
60
+
61
+
case cli.run(script, Nil) {
62
+
Ok(_) -> Nil
63
+
Error(error) -> tailwind.explain(error)
64
+
}
65
+
})
66
+
|> glint.description(description)
67
+
|> glint.unnamed_args(glint.EqArgs(0))
68
+
|> glint.flag("os", {
69
+
let description = ""
70
+
let default = get_os()
71
+
72
+
flag.string()
73
+
|> flag.default(default)
74
+
|> flag.description(description)
75
+
})
76
+
|> glint.flag("cpu", {
77
+
let description = ""
78
+
let default = get_cpu()
79
+
80
+
flag.string()
81
+
|> flag.default(default)
82
+
|> flag.description(description)
83
+
})
84
+
}
85
+
86
+
// EXTERNALS -------------------------------------------------------------------
87
+
88
+
@external(erlang, "lustre_dev_tools_ffi", "get_os")
89
+
fn get_os() -> String
90
+
91
+
@external(erlang, "lustre_dev_tools_ffi", "get_cpu")
92
+
fn get_cpu() -> String
+384
src/lustre_dev_tools/cli/build.gleam
+384
src/lustre_dev_tools/cli/build.gleam
···
1
+
// IMPORTS ---------------------------------------------------------------------
2
+
3
+
import filepath
4
+
import gleam/bool
5
+
import gleam/dict
6
+
import gleam/io
7
+
import gleam/list
8
+
import gleam/package_interface.{type Type, Named, Variable}
9
+
import gleam/result
10
+
import gleam/string
11
+
import glint.{type Command, CommandInput}
12
+
import glint/flag
13
+
import lustre_dev_tools/esbuild
14
+
import lustre_dev_tools/project.{type Module}
15
+
import lustre_dev_tools/cli.{type Cli}
16
+
import lustre_dev_tools/tailwind
17
+
import simplifile
18
+
19
+
// COMMANDS --------------------------------------------------------------------
20
+
21
+
pub fn app() -> Command(Nil) {
22
+
let description =
23
+
"
24
+
Build and bundle an entire Lustre application. The generated JavaScript module
25
+
calls your app's `main` function on page load and can be included in any Web
26
+
page without Gleam or Lustre being present.
27
+
28
+
This is different from using `gleam build` directly because it produces a single
29
+
JavaScript module for you to host or distribute.
30
+
"
31
+
32
+
glint.command(fn(input) {
33
+
let CommandInput(flags: flags, ..) = input
34
+
let assert Ok(minify) = flag.get_bool(flags, "minify")
35
+
36
+
let script = {
37
+
use <- cli.log("Building your project")
38
+
use project_name <- cli.do_result(get_project_name())
39
+
use <- cli.success("Project compiled successfully")
40
+
use <- cli.log("Checking if I can bundle your application")
41
+
use module <- cli.do_result(get_module_interface(project_name))
42
+
use _ <- cli.do_result(check_main_function(project_name, module))
43
+
44
+
use <- cli.log("Creating the bundle entry file")
45
+
let root = project.root()
46
+
let tempdir = filepath.join(root, "build/.lustre")
47
+
let outdir = filepath.join(root, "priv/static")
48
+
let _ = simplifile.create_directory_all(tempdir)
49
+
let _ = simplifile.create_directory_all(outdir)
50
+
use template <- cli.template("entry-with-main.mjs", InternalError)
51
+
let entry = string.replace(template, "{app_name}", project_name)
52
+
53
+
let entryfile = filepath.join(tempdir, "entry.mjs")
54
+
let ext = case minify {
55
+
True -> ".min.mjs"
56
+
False -> ".mjs"
57
+
}
58
+
59
+
let outfile =
60
+
project_name
61
+
|> string.append(ext)
62
+
|> filepath.join(outdir, _)
63
+
64
+
let assert Ok(_) = simplifile.write(entryfile, entry)
65
+
66
+
use _ <- cli.do(bundle(entry, tempdir, outfile, minify))
67
+
use entry <- cli.template("entry.css", InternalError)
68
+
let outfile =
69
+
filepath.strip_extension(outfile)
70
+
|> string.append(".css")
71
+
72
+
let bundle = bundle_tailwind(entry, tempdir, outfile, minify)
73
+
use _ <- cli.do(cli.map_error(bundle, TailwindBundleError))
74
+
75
+
cli.return(Nil)
76
+
}
77
+
78
+
case cli.run(script, Nil) {
79
+
Ok(_) -> Nil
80
+
Error(error) -> explain(error)
81
+
}
82
+
})
83
+
|> glint.description(description)
84
+
|> glint.unnamed_args(glint.EqArgs(0))
85
+
|> glint.flag("minify", {
86
+
let description = "Minify the output"
87
+
let default = False
88
+
89
+
flag.bool()
90
+
|> flag.default(default)
91
+
|> flag.description(description)
92
+
})
93
+
}
94
+
95
+
pub fn component() -> Command(Nil) {
96
+
let description =
97
+
"
98
+
Build a Lustre component as a portable Web Component. The generated JavaScript
99
+
module can be included in any Web page and used without Gleam or Lustre being
100
+
present.
101
+
"
102
+
103
+
glint.command(fn(input) {
104
+
let CommandInput(flags: flags, named_args: args, ..) = input
105
+
let assert Ok(module_path) = dict.get(args, "module_path")
106
+
let assert Ok(minify) = flag.get_bool(flags, "minify")
107
+
108
+
let script = {
109
+
use <- cli.log("Building your project")
110
+
use module <- cli.do_result(get_module_interface(module_path))
111
+
use <- cli.success("Project compiled successfully")
112
+
use <- cli.log("Checking if I can bundle your component")
113
+
use _ <- cli.do_result(check_component_name(module_path, module))
114
+
use component <- cli.do_result(find_component(module_path, module))
115
+
116
+
use <- cli.log("Creating the bundle entry file")
117
+
let root = project.root()
118
+
let tempdir = filepath.join(root, "build/.lustre")
119
+
let outdir = filepath.join(root, "priv/static")
120
+
let _ = simplifile.create_directory_all(tempdir)
121
+
let _ = simplifile.create_directory_all(outdir)
122
+
123
+
use project_name <- cli.do_result(get_project_name())
124
+
125
+
// Esbuild bundling
126
+
use template <- cli.template("component-entry.mjs", InternalError)
127
+
let entry =
128
+
template
129
+
|> string.replace("{component_name}", component)
130
+
|> string.replace("{app_name}", project_name)
131
+
|> string.replace("{module_path}", module_path)
132
+
133
+
let entryfile = filepath.join(tempdir, "entry.mjs")
134
+
let ext = case minify {
135
+
True -> ".min.mjs"
136
+
False -> ".mjs"
137
+
}
138
+
let assert Ok(outfile) =
139
+
string.split(module_path, "/")
140
+
|> list.last
141
+
|> result.map(string.append(_, ext))
142
+
|> result.map(filepath.join(outdir, _))
143
+
144
+
let assert Ok(_) = simplifile.write(entryfile, entry)
145
+
use _ <- cli.do(bundle(entry, tempdir, outfile, minify))
146
+
147
+
// Tailwind bundling
148
+
use entry <- cli.template("entry.css", InternalError)
149
+
let outfile =
150
+
filepath.strip_extension(outfile)
151
+
|> string.append(".css")
152
+
153
+
let bundle = bundle_tailwind(entry, tempdir, outfile, minify)
154
+
use _ <- cli.do(cli.map_error(bundle, TailwindBundleError))
155
+
156
+
cli.return(Nil)
157
+
}
158
+
159
+
case cli.run(script, Nil) {
160
+
Ok(_) -> Nil
161
+
Error(error) -> explain(error)
162
+
}
163
+
})
164
+
|> glint.description(description)
165
+
|> glint.named_args(["module_path"])
166
+
|> glint.unnamed_args(glint.EqArgs(0))
167
+
|> glint.flag("minify", {
168
+
let description = "Minify the output"
169
+
let default = False
170
+
171
+
flag.bool()
172
+
|> flag.default(default)
173
+
|> flag.description(description)
174
+
})
175
+
}
176
+
177
+
// ERROR HANDLING --------------------------------------------------------------
178
+
179
+
type Error {
180
+
BuildError
181
+
BundleError(esbuild.Error)
182
+
TailwindBundleError(tailwind.Error)
183
+
ComponentMissing(module: String)
184
+
MainMissing(module: String)
185
+
ModuleMissing(module: String)
186
+
NameIncorrectType(module: String, got: Type)
187
+
NameMissing(module: String)
188
+
InternalError(message: String)
189
+
}
190
+
191
+
fn explain(error: Error) -> Nil {
192
+
case error {
193
+
BuildError -> project.explain(project.BuildError)
194
+
195
+
BundleError(error) -> esbuild.explain(error)
196
+
197
+
TailwindBundleError(error) -> tailwind.explain(error)
198
+
199
+
ComponentMissing(module) -> io.println("
200
+
Module `" <> module <> "` doesn't have any public function I can use to bundle
201
+
a component.
202
+
203
+
To bundle a component your module should have a public function that returns a
204
+
Lustre `App`:
205
+
206
+
import lustre.{type App}
207
+
pub fn my_component() -> App(flags, model, msg) {
208
+
todo as \"your Lustre component to bundle\"
209
+
}
210
+
")
211
+
212
+
MainMissing(module) -> io.println("
213
+
Module `" <> module <> "` doesn't have a public `main` function I can use as
214
+
the bundle entry point.")
215
+
216
+
ModuleMissing(module) -> io.println("
217
+
I couldn't find a public module called `" <> module <> "` in your project.")
218
+
219
+
NameIncorrectType(module, type_) -> io.println("
220
+
I can't use the `name` constant exposed by module `" <> module <> "`
221
+
to give a name to the component I'm bundling.
222
+
I was expecting `name` to be a `String`,
223
+
but it has type `" <> project.type_to_string(type_) <> "`.")
224
+
225
+
NameMissing(module) -> io.println("
226
+
Module `" <> module <> "` doesn't have a public `name` constant.
227
+
That is required so that I can give a proper name to the component I'm bundling.
228
+
229
+
Try adding a `name` constant to your module like this:
230
+
231
+
const name: String = \"component-name\"")
232
+
233
+
InternalError(message) -> io.println("
234
+
I ran into an error I wasn't expecting. Please open an issue on GitHub at
235
+
https://github.com/lustre-labs/cli with the following message:
236
+
" <> message)
237
+
}
238
+
}
239
+
240
+
// STEPS -----------------------------------------------------------------------
241
+
242
+
fn get_project_name() -> Result(String, Error) {
243
+
project.config()
244
+
|> result.replace_error(BuildError)
245
+
|> result.map(fn(confg) { confg.name })
246
+
}
247
+
248
+
fn get_module_interface(module_path: String) -> Result(Module, Error) {
249
+
project.interface()
250
+
|> result.replace_error(BuildError)
251
+
|> result.then(fn(interface) {
252
+
dict.get(interface.modules, module_path)
253
+
|> result.replace_error(ModuleMissing(module_path))
254
+
})
255
+
}
256
+
257
+
fn check_main_function(
258
+
module_path: String,
259
+
module: Module,
260
+
) -> Result(Nil, Error) {
261
+
case dict.has_key(module.functions, "main") {
262
+
True -> Ok(Nil)
263
+
False -> Error(MainMissing(module_path))
264
+
}
265
+
}
266
+
267
+
fn check_component_name(
268
+
module_path: String,
269
+
module: Module,
270
+
) -> Result(Nil, Error) {
271
+
dict.get(module.constants, "name")
272
+
|> result.replace_error(NameMissing(module_path))
273
+
|> result.then(fn(component_name) {
274
+
case is_string_type(component_name) {
275
+
True -> Ok(Nil)
276
+
False -> Error(NameIncorrectType(module_path, component_name))
277
+
}
278
+
})
279
+
}
280
+
281
+
fn find_component(module_path: String, module: Module) -> Result(String, Error) {
282
+
let functions = dict.to_list(module.functions)
283
+
let error = Error(ComponentMissing(module_path))
284
+
285
+
use _, #(name, t) <- list.fold_until(functions, error)
286
+
case t.parameters, is_compatible_app_type(t.return) {
287
+
[], True -> list.Stop(Ok(name))
288
+
_, _ -> list.Continue(error)
289
+
}
290
+
}
291
+
292
+
fn bundle(
293
+
entry: String,
294
+
tempdir: String,
295
+
outfile: String,
296
+
minify: Bool,
297
+
) -> Cli(any, Nil, Error) {
298
+
let entryfile = filepath.join(tempdir, "entry.mjs")
299
+
let assert Ok(_) = simplifile.write(entryfile, entry)
300
+
use _ <- cli.do(
301
+
esbuild.bundle(entryfile, outfile, minify)
302
+
|> cli.map_error(BundleError),
303
+
)
304
+
305
+
cli.return(Nil)
306
+
}
307
+
308
+
fn bundle_tailwind(
309
+
entry: String,
310
+
tempdir: String,
311
+
outfile: String,
312
+
minify: Bool,
313
+
) -> Cli(any, Nil, tailwind.Error) {
314
+
// We first check if there's a `tailwind.config.js` at the project's root.
315
+
// If not present we do nothing; otherwise we go on with bundling.
316
+
let root = project.root()
317
+
let tailwind_config_file = filepath.join(root, "tailwind.config.js")
318
+
let has_tailwind_config =
319
+
simplifile.verify_is_file(tailwind_config_file)
320
+
|> result.unwrap(False)
321
+
use <- bool.guard(when: !has_tailwind_config, return: cli.return(Nil))
322
+
323
+
use _ <- cli.do(tailwind.setup(get_os(), get_cpu()))
324
+
325
+
use <- cli.log("Bundling with Tailwind")
326
+
let entryfile = filepath.join(tempdir, "entry.css")
327
+
let assert Ok(_) = simplifile.write(entryfile, entry)
328
+
329
+
let flags = ["--watch", "--input=" <> entryfile, "--output=" <> outfile]
330
+
let options = case minify {
331
+
True -> ["--minify", ..flags]
332
+
False -> flags
333
+
}
334
+
use _ <- cli.try(
335
+
cli.exec("./build/.lustre/bin/tailwind", in: root, with: options),
336
+
fn(pair) { tailwind.BundleError(pair.1) },
337
+
)
338
+
use <- cli.success("Bundle produced at `" <> outfile <> "`")
339
+
340
+
cli.return(Nil)
341
+
}
342
+
343
+
// UTILS -----------------------------------------------------------------------
344
+
345
+
fn is_string_type(t: Type) -> Bool {
346
+
case t {
347
+
Named(name: "String", package: "", module: "gleam", parameters: []) -> True
348
+
_ -> False
349
+
}
350
+
}
351
+
352
+
fn is_nil_type(t: Type) -> Bool {
353
+
case t {
354
+
Named(name: "Nil", package: "", module: "gleam", parameters: []) -> True
355
+
_ -> False
356
+
}
357
+
}
358
+
359
+
fn is_type_variable(t: Type) -> Bool {
360
+
case t {
361
+
Variable(..) -> True
362
+
_ -> False
363
+
}
364
+
}
365
+
366
+
fn is_compatible_app_type(t: Type) -> Bool {
367
+
case t {
368
+
Named(
369
+
name: "App",
370
+
package: "lustre",
371
+
module: "lustre",
372
+
parameters: [flags, ..],
373
+
) -> is_nil_type(flags) || is_type_variable(flags)
374
+
_ -> False
375
+
}
376
+
}
377
+
378
+
// EXTERNALS -------------------------------------------------------------------
379
+
380
+
@external(erlang, "cli_ffi", "get_os")
381
+
fn get_os() -> String
382
+
383
+
@external(erlang, "cli_ffi", "get_cpu")
384
+
fn get_cpu() -> String
+257
src/lustre_dev_tools/cli/start.gleam
+257
src/lustre_dev_tools/cli/start.gleam
···
1
+
// IMPORTS ---------------------------------------------------------------------
2
+
3
+
import filepath
4
+
import gleam/dict
5
+
import gleam/io
6
+
import gleam/package_interface.{type Type, Fn, Named, Variable}
7
+
import gleam/result
8
+
import gleam/string
9
+
import glint.{type Command, CommandInput}
10
+
import glint/flag
11
+
import lustre_dev_tools/cli
12
+
import lustre_dev_tools/esbuild
13
+
import lustre_dev_tools/project.{type Module}
14
+
import simplifile
15
+
16
+
// COMMANDS --------------------------------------------------------------------
17
+
18
+
pub fn run() -> Command(Nil) {
19
+
let description =
20
+
"
21
+
"
22
+
23
+
glint.command(fn(input) {
24
+
let CommandInput(flags: flags, ..) = input
25
+
let assert Ok(host) = flag.get_string(flags, "host")
26
+
let assert Ok(port) = flag.get_string(flags, "port")
27
+
let assert Ok(use_lustre_ui) = flag.get_bool(flags, "use-lustre-ui")
28
+
let assert Ok(spa) = flag.get_bool(flags, "spa")
29
+
let custom_html = flag.get_string(flags, "html")
30
+
31
+
let script = {
32
+
use <- cli.log("Building your project")
33
+
use interface <- cli.try(project.interface(), fn(_) { BuildError })
34
+
use module <- cli.try(dict.get(interface.modules, interface.name), fn(_) {
35
+
ModuleMissing(interface.name)
36
+
})
37
+
use is_app <- cli.do_result(check_is_lustre_app(interface.name, module))
38
+
use <- cli.success("Project compiled successfully")
39
+
40
+
use <- cli.log("Creating the application entry point")
41
+
let root = project.root()
42
+
let tempdir = filepath.join(root, "build/.lustre")
43
+
let _ = simplifile.create_directory_all(tempdir)
44
+
use template <- cli.template(
45
+
case is_app {
46
+
True -> "entry-with-start.mjs"
47
+
False -> "entry-with-main.mjs"
48
+
},
49
+
InternalError,
50
+
)
51
+
52
+
let entry = string.replace(template, "{app_name}", interface.name)
53
+
use html <- cli.do(case custom_html {
54
+
Ok(custom_html_path) ->
55
+
custom_html_path
56
+
|> simplifile.read
57
+
|> result.map_error(CouldntOpenCustomHtml(_, custom_html_path))
58
+
|> result.map(string.replace(
59
+
_,
60
+
"<script type=\"application/lustre\">",
61
+
"<script type=\"module\" src=\"./index.mjs\">",
62
+
))
63
+
|> cli.from_result
64
+
65
+
Error(_) if use_lustre_ui -> {
66
+
let name = "index-with-lustre-ui.html"
67
+
use template <- cli.template(name, InternalError)
68
+
let html = string.replace(template, "{app_name}", interface.name)
69
+
70
+
cli.return(html)
71
+
}
72
+
73
+
_ -> {
74
+
let name = "index.html"
75
+
use template <- cli.template(name, InternalError)
76
+
let html = string.replace(template, "{app_name}", interface.name)
77
+
78
+
cli.return(html)
79
+
}
80
+
})
81
+
82
+
let assert Ok(_) = simplifile.write(tempdir <> "/entry.mjs", entry)
83
+
let assert Ok(_) = simplifile.write(tempdir <> "/index.html", html)
84
+
85
+
use _ <- cli.do(
86
+
esbuild.bundle(
87
+
filepath.join(tempdir, "entry.mjs"),
88
+
filepath.join(tempdir, "index.mjs"),
89
+
False,
90
+
)
91
+
|> cli.map_error(BundleError),
92
+
)
93
+
94
+
use _ <- cli.do(
95
+
esbuild.serve(host, port, spa)
96
+
|> cli.map_error(BundleError),
97
+
)
98
+
99
+
cli.return(Nil)
100
+
}
101
+
102
+
case cli.run(script, Nil) {
103
+
Ok(_) -> Nil
104
+
Error(error) -> explain(error)
105
+
}
106
+
})
107
+
|> glint.description(description)
108
+
|> glint.unnamed_args(glint.EqArgs(0))
109
+
|> glint.flag("host", {
110
+
let description = ""
111
+
let default = "localhost"
112
+
113
+
flag.string()
114
+
|> flag.default(default)
115
+
|> flag.description(description)
116
+
})
117
+
|> glint.flag("port", {
118
+
let description = ""
119
+
let default = "1234"
120
+
121
+
flag.string()
122
+
|> flag.default(default)
123
+
|> flag.description(description)
124
+
})
125
+
|> glint.flag("use-lustre-ui", {
126
+
let description = "Inject lustre/ui's stylesheet. Ignored if --html is set."
127
+
let default = False
128
+
129
+
flag.bool()
130
+
|> flag.default(default)
131
+
|> flag.description(description)
132
+
})
133
+
|> glint.flag("spa", {
134
+
let description =
135
+
"Serve your app on any route. Useful for apps that do client-side routing."
136
+
let default = False
137
+
138
+
flag.bool()
139
+
|> flag.default(default)
140
+
|> flag.description(description)
141
+
})
142
+
|> glint.flag("html", {
143
+
let description =
144
+
"Supply a custom HTML file to use as the entry point.
145
+
To inject the Lustre bundle, make sure it includes the following empty script:
146
+
<script type=\"application/lustre\"></script>
147
+
"
148
+
|> string.trim_right
149
+
150
+
flag.string()
151
+
|> flag.description(description)
152
+
})
153
+
}
154
+
155
+
// ERROR HANDLING --------------------------------------------------------------
156
+
157
+
type Error {
158
+
BuildError
159
+
BundleError(esbuild.Error)
160
+
CouldntOpenCustomHtml(error: simplifile.FileError, path: String)
161
+
MainMissing(module: String)
162
+
MainIncorrectType(module: String, got: Type)
163
+
MainBadAppType(module: String, got: Type)
164
+
ModuleMissing(module: String)
165
+
InternalError(message: String)
166
+
}
167
+
168
+
fn explain(error: Error) -> Nil {
169
+
case error {
170
+
BuildError -> project.explain(project.BuildError)
171
+
172
+
BundleError(error) -> esbuild.explain(error)
173
+
174
+
CouldntOpenCustomHtml(_, path) -> io.println("
175
+
I couldn't open the custom HTML file at `" <> path <> "`.")
176
+
177
+
MainMissing(module) -> io.println("
178
+
Module `" <> module <> "` doesn't have a public `main` function I can preview.")
179
+
180
+
MainIncorrectType(module, type_) -> io.println("
181
+
I cannot preview the `main` function exposed by module `" <> module <> "`.
182
+
To start a preview server I need it to take no arguments and return a Lustre
183
+
`App`.
184
+
The one I found has type `" <> project.type_to_string(type_) <> "`.")
185
+
186
+
// TODO: maybe this could have useful links to `App`/flags...
187
+
MainBadAppType(module, type_) -> io.println("
188
+
I cannot preview the `main` function exposed by module `" <> module <> "`.
189
+
To start a preview server I need it to return a Lustre `App` that doesn't need
190
+
any flags.
191
+
The one I found has type `" <> project.type_to_string(type_) <> "`.
192
+
193
+
Its return type should look something like this:
194
+
195
+
import lustre.{type App}
196
+
pub fn main() -> App(flags, model, msg) {
197
+
todo as \"your Lustre application to preview\"
198
+
}")
199
+
200
+
ModuleMissing(module) -> io.println("
201
+
I couldn't find a public module called `" <> module <> "` in your project.")
202
+
203
+
InternalError(message) -> io.println("
204
+
I ran into an error I wasn't expecting. Please open an issue on GitHub at
205
+
https://github.com/lustre-labs/cli with the following message:
206
+
" <> message)
207
+
}
208
+
}
209
+
210
+
// STEPS -----------------------------------------------------------------------
211
+
212
+
fn check_is_lustre_app(
213
+
module_path: String,
214
+
module: Module,
215
+
) -> Result(Bool, Error) {
216
+
dict.get(module.functions, "main")
217
+
|> result.replace_error(MainMissing(module_path))
218
+
|> result.then(fn(main) {
219
+
case main.parameters, main.return {
220
+
[_, ..], _ ->
221
+
Error(MainIncorrectType(module_path, Fn(main.parameters, main.return)))
222
+
223
+
[], Named(
224
+
name: "App",
225
+
package: "lustre",
226
+
module: "lustre",
227
+
parameters: [flags, ..],
228
+
) ->
229
+
case is_compatible_flags_type(flags) {
230
+
True -> Ok(True)
231
+
False -> Error(MainBadAppType(module_path, main.return))
232
+
}
233
+
234
+
[], _ -> Ok(False)
235
+
}
236
+
})
237
+
}
238
+
239
+
// UTILS -----------------------------------------------------------------------
240
+
241
+
fn is_nil_type(t: Type) -> Bool {
242
+
case t {
243
+
Named(name: "Nil", package: "", module: "gleam", parameters: []) -> True
244
+
_ -> False
245
+
}
246
+
}
247
+
248
+
fn is_type_variable(t: Type) -> Bool {
249
+
case t {
250
+
Variable(..) -> True
251
+
_ -> False
252
+
}
253
+
}
254
+
255
+
fn is_compatible_flags_type(t: Type) -> Bool {
256
+
is_nil_type(t) || is_type_variable(t)
257
+
}
+247
src/lustre_dev_tools/esbuild.gleam
+247
src/lustre_dev_tools/esbuild.gleam
···
1
+
// IMPORTS ---------------------------------------------------------------------
2
+
3
+
import filepath
4
+
import gleam/dynamic.{type Dynamic}
5
+
import gleam/io
6
+
import gleam/list
7
+
import gleam/result
8
+
import gleam/set
9
+
import gleam/string
10
+
import lustre_dev_tools/cli.{type Cli}
11
+
import lustre_dev_tools/project
12
+
import simplifile.{type FilePermissions, Execute, FilePermissions, Read, Write}
13
+
14
+
// COMMANDS --------------------------------------------------------------------
15
+
16
+
pub fn download(os: String, cpu: String) -> Cli(any, Nil, Error) {
17
+
use <- cli.log("Downloading esbuild")
18
+
19
+
let outdir = filepath.join(project.root(), "build/.lustre/bin")
20
+
let outfile = filepath.join(outdir, "esbuild")
21
+
22
+
case check_esbuild_exists(outfile) {
23
+
True -> cli.success("Esbuild already installed!", fn() { cli.return(Nil) })
24
+
False -> {
25
+
use <- cli.log("Detecting platform")
26
+
use url <- cli.do_result(get_download_url(os, cpu))
27
+
28
+
use <- cli.log("Downloading from " <> url)
29
+
use tarball <- cli.try(get_esbuild(url), NetworkError)
30
+
31
+
use <- cli.log("Unzipping esbuild")
32
+
use bin <- cli.try(unzip_esbuild(tarball), UnzipError)
33
+
use _ <- cli.try(write_esbuild(bin, outdir, outfile), SimplifileError(
34
+
_,
35
+
outfile,
36
+
))
37
+
use _ <- cli.try(set_filepermissions(outfile), SimplifileError(_, outfile))
38
+
39
+
use <- cli.success("Esbuild installed!")
40
+
cli.return(Nil)
41
+
}
42
+
}
43
+
}
44
+
45
+
pub fn bundle(
46
+
input_file: String,
47
+
output_file: String,
48
+
minify: Bool,
49
+
) -> Cli(any, Nil, Error) {
50
+
use _ <- cli.do(download(get_os(), get_cpu()))
51
+
use _ <- cli.try(project.build(), fn(_) { BuildError })
52
+
use <- cli.log("Getting everything ready for tree shaking")
53
+
54
+
let root = project.root()
55
+
use _ <- cli.try(configure_node_tree_shaking(root), SimplifileError(_, root))
56
+
57
+
let flags = [
58
+
"--bundle",
59
+
"--external:node:*",
60
+
"--format=esm",
61
+
"--outfile=" <> output_file,
62
+
]
63
+
let options = case minify {
64
+
True -> [input_file, "--minify", ..flags]
65
+
False -> [input_file, ..flags]
66
+
}
67
+
68
+
use <- cli.log("Bundling with esbuild")
69
+
use _ <- cli.try(
70
+
cli.exec(run: "./build/.lustre/bin/esbuild", in: root, with: options),
71
+
fn(pair) { BundleError(pair.1) },
72
+
)
73
+
74
+
use <- cli.success("Bundle produced at `" <> output_file <> "`")
75
+
cli.return(Nil)
76
+
}
77
+
78
+
pub fn serve(host: String, port: String, spa: Bool) -> Cli(any, Nil, Error) {
79
+
use _ <- cli.do(download(get_os(), get_cpu()))
80
+
let root = project.root()
81
+
let flags = [
82
+
"--serve=" <> host <> ":" <> port,
83
+
"--servedir=" <> filepath.join(root, "build/.lustre"),
84
+
]
85
+
86
+
let options = case spa {
87
+
True -> [
88
+
"--serve-fallback=" <> filepath.join(root, "build/.lustre/index.html"),
89
+
..flags
90
+
]
91
+
False -> flags
92
+
}
93
+
94
+
use <- cli.success("Starting dev server at " <> host <> ":" <> port <> "...")
95
+
use _ <- cli.try(
96
+
cli.exec(run: "./build/.lustre/bin/esbuild", in: root, with: options),
97
+
fn(pair) { BundleError(pair.1) },
98
+
)
99
+
100
+
cli.return(Nil)
101
+
}
102
+
103
+
// STEPS -----------------------------------------------------------------------
104
+
105
+
fn check_esbuild_exists(path) {
106
+
case simplifile.verify_is_file(path) {
107
+
Ok(True) -> True
108
+
Ok(False) | Error(_) -> False
109
+
}
110
+
}
111
+
112
+
fn get_download_url(os, cpu) {
113
+
let base = "https://registry.npmjs.org/@esbuild/"
114
+
let path = case os, cpu {
115
+
"android", "arm" -> Ok("android-arm/-/android-arm-0.19.10.tgz")
116
+
"android", "arm64" -> Ok("android-arm64/-/android-arm64-0.19.10.tgz")
117
+
"android", "x64" -> Ok("android-x64/-/android-x64-0.19.10.tgz")
118
+
119
+
"darwin", "aarch64" -> Ok("darwin-arm64/-/darwin-arm64-0.19.10.tgz")
120
+
"darwin", "amd64" -> Ok("darwin-arm64/-/darwin-arm64-0.19.10.tgz")
121
+
"darwin", "arm64" -> Ok("darwin-arm64/-/darwin-arm64-0.19.10.tgz")
122
+
"darwin", "x86_64" -> Ok("darwin-x64/-/darwin-x64-0.19.10.tgz")
123
+
124
+
"freebsd", "arm64" -> Ok("freebsd-arm64/-/freebsd-arm64-0.19.10.tgz")
125
+
"freebsd", "x64" -> Ok("freebsd-x64/-/freebsd-x64-0.19.10.tgz")
126
+
127
+
"linux", "aarch64" -> Ok("linux-arm64/-/linux-arm64-0.19.10.tgz")
128
+
"linux", "arm" -> Ok("linux-arm/-/linux-arm-0.19.10.tgz")
129
+
"linux", "arm64" -> Ok("linux-arm64/-/linux-arm64-0.19.10.tgz")
130
+
"linux", "ia32" -> Ok("linux-ia32/-/linux-ia32-0.19.10.tgz")
131
+
"linux", "x64" -> Ok("linux-x64/-/linux-x64-0.19.10.tgz")
132
+
"linux", "x86_64" -> Ok("linux-x64/-/linux-x64-0.19.10.tgz")
133
+
134
+
"win32", "arm64" -> Ok("win32-arm64/-/win32-arm64-0.19.10.tgz")
135
+
"win32", "ia32" -> Ok("win32-ia32/-/win32-ia32-0.19.10.tgz")
136
+
"win32", "x64" -> Ok("win32-x64/-/win32-x64-0.19.10.tgz")
137
+
"win32", "x86_64" -> Ok("win32-x64/-/win32-x64-0.19.10.tgz")
138
+
139
+
"netbsd", "x64" -> Ok("netbsd-x64/-/netbsd-x64-0.19.10.tgz")
140
+
"openbsd", "x64" -> Ok("openbsd-x64/-/openbsd-x64-0.19.10.tgz")
141
+
"sunos", "x64" -> Ok("sunos-x64/-/sunos-x64-0.19.10.tgz")
142
+
143
+
_, _ -> Error(UnknownPlatform(os, cpu))
144
+
}
145
+
146
+
result.map(path, string.append(base, _))
147
+
}
148
+
149
+
fn write_esbuild(bin, outdir, outfile) {
150
+
let _ = simplifile.create_directory_all(outdir)
151
+
152
+
simplifile.write_bits(outfile, bin)
153
+
}
154
+
155
+
fn set_filepermissions(file) {
156
+
let permissions =
157
+
FilePermissions(
158
+
user: set.from_list([Read, Write, Execute]),
159
+
group: set.from_list([Read, Execute]),
160
+
other: set.from_list([Read, Execute]),
161
+
)
162
+
163
+
simplifile.set_permissions(file, permissions)
164
+
}
165
+
166
+
fn configure_node_tree_shaking(root) {
167
+
// This whole chunk of code is to force tree shaking on dependencies that esbuild
168
+
// has a habit of including because it thinks their imports might have side
169
+
// effects.
170
+
//
171
+
// This is a really grim hack but it's the only way I've found to get esbuild to
172
+
// ignore unused deps like `glint` that imports node stuff but aren't used in
173
+
// app code.
174
+
let force_tree_shaking = "{ \"sideEffects\": false }"
175
+
let assert Ok(_) =
176
+
simplifile.write(
177
+
filepath.join(root, "build/dev/javascript/package.json"),
178
+
force_tree_shaking,
179
+
)
180
+
let pure_deps = ["lustre", "glint", "simplifile"]
181
+
182
+
list.try_each(pure_deps, fn(dep) {
183
+
root
184
+
|> filepath.join("build/dev/javascript/" <> dep)
185
+
|> filepath.join("package.json")
186
+
|> simplifile.write(force_tree_shaking)
187
+
})
188
+
}
189
+
190
+
// ERROR HANDLING --------------------------------------------------------------
191
+
192
+
pub type Error {
193
+
BuildError
194
+
BundleError(message: String)
195
+
NetworkError(Dynamic)
196
+
SimplifileError(reason: simplifile.FileError, path: String)
197
+
UnknownPlatform(os: String, cpu: String)
198
+
UnzipError(Dynamic)
199
+
}
200
+
201
+
pub fn explain(error: Error) -> Nil {
202
+
case error {
203
+
BuildError -> project.explain(project.BuildError)
204
+
205
+
BundleError(message) -> io.println("
206
+
I ran into an error while trying to create a bundle with esbuild:
207
+
" <> message)
208
+
209
+
// TODO: Is there a better way to deal with this dynamic error?
210
+
NetworkError(_dynamic) ->
211
+
io.println(
212
+
"
213
+
There was a network error!",
214
+
)
215
+
216
+
// TODO: this could give a better error for some common reason like Enoent.
217
+
SimplifileError(reason, path) -> io.println("
218
+
I ran into the following error at path `" <> path <> "`:" <> string.inspect(
219
+
reason,
220
+
) <> ".")
221
+
222
+
UnknownPlatform(os, cpu) -> io.println("
223
+
I couldn't figure out the correct esbuild version for your
224
+
os (" <> os <> ") and cpu (" <> cpu <> ").")
225
+
226
+
// TODO: Is there a better way to deal with this dynamic error?
227
+
UnzipError(_dynamic) ->
228
+
io.println(
229
+
"
230
+
I couldn't unzip the esbuild executable!",
231
+
)
232
+
}
233
+
}
234
+
235
+
// EXTERNALS -------------------------------------------------------------------
236
+
237
+
@external(erlang, "lustre_dev_tools_ffi", "get_os")
238
+
fn get_os() -> String
239
+
240
+
@external(erlang, "lustre_dev_tools_ffi", "get_cpu")
241
+
fn get_cpu() -> String
242
+
243
+
@external(erlang, "lustre_dev_tools_ffi", "get_esbuild")
244
+
fn get_esbuild(url: String) -> Result(BitArray, Dynamic)
245
+
246
+
@external(erlang, "lustre_dev_tools_ffi", "unzip_esbuild")
247
+
fn unzip_esbuild(tarball: BitArray) -> Result(BitArray, Dynamic)
+179
src/lustre_dev_tools/project.gleam
+179
src/lustre_dev_tools/project.gleam
···
1
+
// IMPORTS ---------------------------------------------------------------------
2
+
3
+
import filepath
4
+
import gleam/dict.{type Dict}
5
+
import gleam/dynamic.{type DecodeError, type Decoder, type Dynamic, DecodeError}
6
+
import gleam/int
7
+
import gleam/io
8
+
import gleam/json
9
+
import gleam/list
10
+
import gleam/package_interface.{type Type, Fn, Named, Tuple, Variable}
11
+
import gleam/pair
12
+
import gleam/result
13
+
import gleam/string
14
+
import lustre_dev_tools/cli
15
+
import simplifile
16
+
import tom.{type Toml}
17
+
18
+
// TYPES -----------------------------------------------------------------------
19
+
20
+
pub type Config {
21
+
Config(name: String, version: String, toml: Dict(String, Toml))
22
+
}
23
+
24
+
pub type Interface {
25
+
Interface(name: String, version: String, modules: Dict(String, Module))
26
+
}
27
+
28
+
pub type Module {
29
+
Module(constants: Dict(String, Type), functions: Dict(String, Function))
30
+
}
31
+
32
+
pub type Function {
33
+
Function(parameters: List(Type), return: Type)
34
+
}
35
+
36
+
// COMMANDS --------------------------------------------------------------------
37
+
38
+
/// Compile the current project running the `gleam build` command.
39
+
///
40
+
pub fn build() -> Result(Nil, String) {
41
+
cli.exec(run: "gleam", in: ".", with: ["build", "--target=js"])
42
+
|> result.map_error(pair.second)
43
+
|> result.replace(Nil)
44
+
}
45
+
46
+
pub fn interface() -> Result(Interface, String) {
47
+
let dir = filepath.join(root(), "build/.lustre")
48
+
let out = filepath.join(dir, "package-interface.json")
49
+
let args = ["export", "package-interface", "--out", out]
50
+
51
+
cli.exec(run: "gleam", in: ".", with: args)
52
+
|> result.map_error(pair.second)
53
+
|> result.then(fn(_) {
54
+
let assert Ok(json) = simplifile.read(out)
55
+
let assert Ok(interface) = json.decode(json, interface_decoder)
56
+
57
+
Ok(interface)
58
+
})
59
+
}
60
+
61
+
/// Read the project configuration in the `gleam.toml` file.
62
+
///
63
+
pub fn config() -> Result(Config, String) {
64
+
use _ <- result.try(build())
65
+
66
+
// Since we made sure that the project could compile we're sure that there is
67
+
// bound to be a `gleam.toml` file somewhere in the current directory (or in
68
+
// its parent directories). So we can safely call `root()` without
69
+
// it looping indefinitely.
70
+
let configuration_path = filepath.join(root(), "gleam.toml")
71
+
72
+
// All these operations are safe to assert because the Gleam project wouldn't
73
+
// compile if any of this stuff was invalid.
74
+
let assert Ok(configuration) = simplifile.read(configuration_path)
75
+
let assert Ok(toml) = tom.parse(configuration)
76
+
let assert Ok(name) = tom.get_string(toml, ["name"])
77
+
let assert Ok(version) = tom.get_string(toml, ["version"])
78
+
79
+
Ok(Config(name: name, version: version, toml: toml))
80
+
}
81
+
82
+
// ERROR HANDLING --------------------------------------------------------------
83
+
84
+
///
85
+
///
86
+
pub type Error {
87
+
BuildError
88
+
}
89
+
90
+
pub fn explain(error: Error) -> Nil {
91
+
case error {
92
+
BuildError ->
93
+
"
94
+
It looks like your project has some compilation errors that need to be addressed
95
+
before I can do anything."
96
+
|> io.println
97
+
}
98
+
}
99
+
100
+
// UTILS -----------------------------------------------------------------------
101
+
102
+
/// Finds the path leading to the project's root folder. This recursively walks
103
+
/// up from the current directory until it finds a `gleam.toml`.
104
+
///
105
+
pub fn root() -> String {
106
+
find_root(".")
107
+
}
108
+
109
+
fn find_root(path: String) -> String {
110
+
let toml = filepath.join(path, "gleam.toml")
111
+
112
+
case simplifile.verify_is_file(toml) {
113
+
Ok(False) | Error(_) -> find_root(filepath.join("..", path))
114
+
Ok(True) -> path
115
+
}
116
+
}
117
+
118
+
pub fn type_to_string(type_: Type) -> String {
119
+
case type_ {
120
+
Tuple(elements) -> {
121
+
let elements = list.map(elements, type_to_string)
122
+
"#(" <> string.join(elements, with: ", ") <> ")"
123
+
}
124
+
125
+
Fn(params, return) -> {
126
+
let params = list.map(params, type_to_string)
127
+
let return = type_to_string(return)
128
+
"fn(" <> string.join(params, with: ", ") <> ") -> " <> return
129
+
}
130
+
131
+
Named(name, _package, _module, []) -> name
132
+
Named(name, _package, _module, params) -> {
133
+
let params = list.map(params, type_to_string)
134
+
name <> "(" <> string.join(params, with: ", ") <> ")"
135
+
}
136
+
137
+
Variable(id) -> "a_" <> int.to_string(id)
138
+
}
139
+
}
140
+
141
+
// DECODERS --------------------------------------------------------------------
142
+
143
+
fn interface_decoder(dyn: Dynamic) -> Result(Interface, List(DecodeError)) {
144
+
dynamic.decode3(
145
+
Interface,
146
+
dynamic.field("name", dynamic.string),
147
+
dynamic.field("version", dynamic.string),
148
+
dynamic.field("modules", string_dict(module_decoder)),
149
+
)(dyn)
150
+
}
151
+
152
+
fn module_decoder(dyn: Dynamic) -> Result(Module, List(DecodeError)) {
153
+
dynamic.decode2(
154
+
Module,
155
+
dynamic.field(
156
+
"constants",
157
+
string_dict(dynamic.field("type", package_interface.type_decoder)),
158
+
),
159
+
dynamic.field("functions", string_dict(function_decoder)),
160
+
)(dyn)
161
+
}
162
+
163
+
fn function_decoder(dyn: Dynamic) -> Result(Function, List(DecodeError)) {
164
+
dynamic.decode2(
165
+
Function,
166
+
dynamic.field("parameters", dynamic.list(labelled_argument_decoder)),
167
+
dynamic.field("return", package_interface.type_decoder),
168
+
)(dyn)
169
+
}
170
+
171
+
fn labelled_argument_decoder(dyn: Dynamic) -> Result(Type, List(DecodeError)) {
172
+
// In this case we don't really care about the label, so we're just ignoring
173
+
// it and returning the argument's type.
174
+
dynamic.field("type", package_interface.type_decoder)(dyn)
175
+
}
176
+
177
+
fn string_dict(values: Decoder(a)) -> Decoder(Dict(String, a)) {
178
+
dynamic.dict(dynamic.string, values)
179
+
}
+179
src/lustre_dev_tools/tailwind.gleam
+179
src/lustre_dev_tools/tailwind.gleam
···
1
+
// IMPORTS ---------------------------------------------------------------------
2
+
3
+
import filepath
4
+
import gleam/bool
5
+
import gleam/dynamic.{type Dynamic}
6
+
import gleam/io
7
+
import gleam/result
8
+
import gleam/set
9
+
import gleam/string
10
+
import lustre_dev_tools/project
11
+
import lustre_dev_tools/cli.{type Cli}
12
+
import simplifile.{type FilePermissions, Execute, FilePermissions, Read, Write}
13
+
14
+
const tailwind_version = "v3.4.1"
15
+
16
+
// COMMANDS --------------------------------------------------------------------
17
+
18
+
pub fn setup(os: String, cpu: String) -> Cli(any, Nil, Error) {
19
+
use _ <- cli.do(download(os, cpu, tailwind_version))
20
+
use _ <- cli.do(write_tailwind_config())
21
+
22
+
cli.return(Nil)
23
+
}
24
+
25
+
fn download(os: String, cpu: String, version: String) -> Cli(any, Nil, Error) {
26
+
use <- cli.log("Downloading Tailwind")
27
+
28
+
let root = project.root()
29
+
let outdir = filepath.join(root, "build/.lustre/bin")
30
+
let outfile = filepath.join(outdir, "tailwind")
31
+
32
+
case check_tailwind_exists(outfile) {
33
+
True -> cli.success("Tailwind already installed!", fn() { cli.return(Nil) })
34
+
False -> {
35
+
use <- cli.log("Detecting platform")
36
+
use url <- cli.do_result(get_download_url(os, cpu, version))
37
+
38
+
use <- cli.log("Downloading from " <> url)
39
+
use bin <- cli.try(get_tailwind(url), NetworkError)
40
+
41
+
use _ <- cli.try(write_tailwind(bin, outdir, outfile), fn(reason) {
42
+
CannotWriteTailwind(reason, outfile)
43
+
})
44
+
use _ <- cli.try(set_filepermissions(outfile), fn(reason) {
45
+
CannotSetPermissions(reason, outfile)
46
+
})
47
+
48
+
use <- cli.success("Tailwind installed!")
49
+
50
+
cli.return(Nil)
51
+
}
52
+
}
53
+
}
54
+
55
+
fn write_tailwind_config() -> Cli(any, Nil, Error) {
56
+
let config_filename = "tailwind.config.js"
57
+
let config_outfile = filepath.join(project.root(), config_filename)
58
+
let config_already_exists =
59
+
simplifile.verify_is_file(config_outfile)
60
+
|> result.unwrap(False)
61
+
62
+
// If there already is a configuration file, we make sure not to override it.
63
+
use <- bool.guard(when: config_already_exists, return: cli.return(Nil))
64
+
use <- cli.log("Writing `" <> config_filename <> "`")
65
+
use config <- cli.template("tailwind.config.js", InternalError)
66
+
use _ <- cli.try(
67
+
simplifile.write(to: config_outfile, contents: config),
68
+
CannotWriteConfig(_, config_outfile),
69
+
)
70
+
use <- cli.success("Written `" <> config_outfile <> "`")
71
+
72
+
cli.return(Nil)
73
+
}
74
+
75
+
// STEPS -----------------------------------------------------------------------
76
+
77
+
fn check_tailwind_exists(path) {
78
+
case simplifile.verify_is_file(path) {
79
+
Ok(True) -> True
80
+
Ok(False) | Error(_) -> False
81
+
}
82
+
}
83
+
84
+
fn get_download_url(os, cpu, version) {
85
+
let base =
86
+
"https://github.com/tailwindlabs/tailwindcss/releases/download/"
87
+
<> version
88
+
<> "/tailwindcss-"
89
+
90
+
let path = case os, cpu {
91
+
"linux", "armv7" -> Ok("linux-armv7")
92
+
"linux", "arm64" -> Ok("linux-arm64")
93
+
"linux", "x64" | "linux", "x86_64" -> Ok("linux-x64")
94
+
95
+
"win32", "arm64" -> Ok("windows-arm64.exe")
96
+
"win32", "x64" | "win32", "x86_64" -> Ok("windows-x64.exe")
97
+
98
+
"darwin", "arm64" | "darwin", "aarch64" -> Ok("macos-arm64")
99
+
"darwin", "x64" | "darwin", "x86_64" -> Ok("macos-x64")
100
+
101
+
_, _ -> Error(UnknownPlatform(os, cpu))
102
+
}
103
+
104
+
result.map(path, string.append(base, _))
105
+
}
106
+
107
+
fn write_tailwind(bin, outdir, outfile) {
108
+
let _ = simplifile.create_directory_all(outdir)
109
+
110
+
simplifile.write_bits(outfile, bin)
111
+
}
112
+
113
+
fn set_filepermissions(file) {
114
+
let permissions =
115
+
FilePermissions(
116
+
user: set.from_list([Read, Write, Execute]),
117
+
group: set.from_list([Read, Execute]),
118
+
other: set.from_list([Read, Execute]),
119
+
)
120
+
121
+
simplifile.set_permissions(file, permissions)
122
+
}
123
+
124
+
// ERROR HANDLING --------------------------------------------------------------
125
+
126
+
pub type Error {
127
+
NetworkError(Dynamic)
128
+
CannotWriteTailwind(reason: simplifile.FileError, path: String)
129
+
CannotSetPermissions(reason: simplifile.FileError, path: String)
130
+
CannotWriteConfig(reason: simplifile.FileError, path: String)
131
+
UnknownPlatform(os: String, cpu: String)
132
+
BundleError(reason: String)
133
+
InternalError(message: String)
134
+
}
135
+
136
+
pub fn explain(error: Error) -> Nil {
137
+
case error {
138
+
// TODO: Is there a better way to deal with this dynamic error?
139
+
NetworkError(_dynamic) ->
140
+
io.println(
141
+
"
142
+
There was a network error!",
143
+
)
144
+
145
+
UnknownPlatform(os, cpu) -> io.println("
146
+
I couldn't figure out the correct Tailwind version for your
147
+
os (" <> os <> ") and cpu (" <> cpu <> ").")
148
+
149
+
CannotSetPermissions(reason, _) -> io.println("
150
+
I ran into an error (" <> string.inspect(reason) <> ") when trying
151
+
to set permissions for the Tailwind executable.
152
+
")
153
+
154
+
CannotWriteConfig(reason, _) -> io.println("
155
+
I ran into an error (" <> string.inspect(reason) <> ") when trying
156
+
to write the `tailwind.config.js` file to the project's root.
157
+
")
158
+
159
+
CannotWriteTailwind(reason, path) -> io.println("
160
+
I ran into an error (" <> string.inspect(reason) <> ") when trying
161
+
to write the Tailwind binary to
162
+
`" <> path <> "`.
163
+
")
164
+
165
+
BundleError(reason) -> io.println("
166
+
I ran into an error while trying to create a bundle with Tailwind:
167
+
" <> reason)
168
+
169
+
InternalError(message) -> io.println("
170
+
I ran into an error I wasn't expecting. Please open an issue on GitHub at
171
+
https://github.com/lustre-labs/cli with the following message:
172
+
" <> message)
173
+
}
174
+
}
175
+
176
+
// EXTERNALS -------------------------------------------------------------------
177
+
178
+
@external(erlang, "lustre_dev_tools_ffi", "get_esbuild")
179
+
fn get_tailwind(url: String) -> Result(BitArray, Dynamic)
+86
src/lustre_dev_tools_ffi.erl
+86
src/lustre_dev_tools_ffi.erl
···
1
+
-module(lustre_dev_tools_ffi).
2
+
-export([
3
+
get_cpu/0,
4
+
get_esbuild/1,
5
+
get_tailwind/1,
6
+
get_os/0,
7
+
unzip_esbuild/1,
8
+
exec/3
9
+
]).
10
+
11
+
get_os() ->
12
+
case os:type() of
13
+
{win32, _} -> <<"win32">>;
14
+
{unix, darwin} -> <<"darwin">>;
15
+
{unix, linux} -> <<"linux">>;
16
+
{_, Unknown} -> atom_to_binary(Unknown, utf8)
17
+
end.
18
+
19
+
get_cpu() ->
20
+
case erlang:system_info(os_type) of
21
+
{unix, _} ->
22
+
[Arch, _] = string:split(erlang:system_info(system_architecture), "-"),
23
+
list_to_binary(Arch);
24
+
{win32, _} ->
25
+
case erlang:system_info(wordsize) of
26
+
4 -> {ok, <<"ia32">>};
27
+
8 -> {ok, <<"x64">>}
28
+
end
29
+
end.
30
+
31
+
get_esbuild(Url) ->
32
+
inets:start(),
33
+
ssl:start(),
34
+
35
+
case httpc:request(get, {Url, []}, [], [{body_format, binary}]) of
36
+
{ok, {{_, 200, _}, _, Zip}} -> {ok, Zip};
37
+
{ok, Res} -> {error, Res};
38
+
{error, Err} -> {error, Err}
39
+
end.
40
+
41
+
get_tailwind(Url) ->
42
+
inets:start(),
43
+
ssl:start(),
44
+
45
+
case httpc:request(get, {Url, []}, [], [{body_format, binary}]) of
46
+
{ok, {{_, 200, _}, _, Bin}} -> {ok, Bin};
47
+
{ok, Res} -> {error, Res};
48
+
{error, Err} -> {error, Err}
49
+
end.
50
+
51
+
unzip_esbuild(Zip) ->
52
+
Result =
53
+
erl_tar:extract({binary, Zip}, [
54
+
memory, compressed, {files, ["package/bin/esbuild"]}
55
+
]),
56
+
57
+
case Result of
58
+
{ok, [{_, Esbuild}]} -> {ok, Esbuild};
59
+
{ok, Res} -> {error, Res};
60
+
{error, Err} -> {error, Err}
61
+
end.
62
+
63
+
exec(Command, Args, Cwd) ->
64
+
Command_ = binary_to_list(Command),
65
+
Args_ = lists:map(fun(Arg) -> binary_to_list(Arg) end, Args),
66
+
Cwd_ = binary_to_list(Cwd),
67
+
68
+
Name = case Command_ of
69
+
"./" ++ _ -> {spawn_executable, Command_};
70
+
"/" ++ _ -> {spawn_executable, Command_};
71
+
_ -> {spawn_executable, os:find_executable(Command_)}
72
+
end,
73
+
74
+
Port = open_port(Name, [exit_status, binary, hide, stream, eof,
75
+
{args, Args_},
76
+
{cd, Cwd_}
77
+
]),
78
+
79
+
do_exec(Port, []).
80
+
81
+
do_exec(Port, Acc) ->
82
+
receive
83
+
{Port, {data, Data}} -> do_exec(Port, [Data | Acc]);
84
+
{Port, {exit_status, 0}} -> {ok, list_to_binary(lists:reverse(Acc))};
85
+
{Port, {exit_status, Code}} -> {error, {Code, list_to_binary(lists:reverse(Acc))}}
86
+
end.