///|
fn[T : Debug] write_debug_value(value : T, logger : &Logger) -> Unit {
logger.write_string(Debug::to_repr(value).to_string())
}
///|
pub(all) enum ServerExecutor {
Wagi
Workerd
} derive(Debug, Eq)
///|
pub impl Show for ServerExecutor with fn output(self, logger) {
match self {
Wagi => logger.write_string("Wagi")
Workerd => logger.write_string("Workerd")
}
}
///|
pub(all) struct KvNamespaceBinding {
binding : String
id : String
} derive(Debug, Eq)
///|
pub impl Show for KvNamespaceBinding with fn output(self, logger) {
logger.write_string("{binding: ")
Show::output(self.binding, logger)
logger.write_string(", id: ")
Show::output(self.id, logger)
logger.write_string("}")
}
///|
pub(all) struct R2BucketBinding {
binding : String
bucket_name : String
} derive(Debug, Eq)
///|
pub impl Show for R2BucketBinding with fn output(self, logger) {
logger.write_string("{binding: ")
Show::output(self.binding, logger)
logger.write_string(", bucket_name: ")
Show::output(self.bucket_name, logger)
logger.write_string("}")
}
///|
pub(all) struct D1DatabaseBinding {
binding : String
database_name : String
database_id : String
} derive(Debug, Eq)
///|
pub impl Show for D1DatabaseBinding with fn output(self, logger) {
logger.write_string("{binding: ")
Show::output(self.binding, logger)
logger.write_string(", database_name: ")
Show::output(self.database_name, logger)
logger.write_string(", database_id: ")
Show::output(self.database_id, logger)
logger.write_string("}")
}
///|
pub(all) struct ServerBindings {
kv_namespaces : Array[KvNamespaceBinding]
r2_buckets : Array[R2BucketBinding]
d1_databases : Array[D1DatabaseBinding]
} derive(Debug, Eq)
///|
pub impl Show for ServerBindings with fn output(self, logger) {
logger.write_string("{kv_namespaces: ")
write_debug_value(self.kv_namespaces, logger)
logger.write_string(", r2_buckets: ")
write_debug_value(self.r2_buckets, logger)
logger.write_string(", d1_databases: ")
write_debug_value(self.d1_databases, logger)
logger.write_string("}")
}
///|
pub fn empty_server_bindings() -> ServerBindings {
{ kv_namespaces: [], r2_buckets: [], d1_databases: [] }
}
///|
pub struct ServerConfig {
executor : ServerExecutor
route : String
bindings : ServerBindings
} derive(Debug)
///|
pub impl Show for ServerConfig with fn output(self, logger) {
logger.write_string("{executor: ")
Show::output(self.executor, logger)
logger.write_string(", route: ")
Show::output(self.route, logger)
logger.write_string(", bindings: ")
Show::output(self.bindings, logger)
logger.write_string("}")
}
///|
pub fn parse_server_bindings(
json_value : Json,
) -> Result[ServerBindings, String] {
match json_value {
Object(obj) => {
let kv_namespaces : Array[KvNamespaceBinding] = []
let r2_buckets : Array[R2BucketBinding] = []
let d1_databases : Array[D1DatabaseBinding] = []
if obj.get("kv_namespaces") is Some(Array(arr)) {
for item in arr {
match item {
String(name) =>
kv_namespaces.push({
binding: name,
id: "wite-dev-" + name.to_lower(),
})
Object(o) => {
let binding = match o.get("binding") {
Some(String(s)) => s
_ => return Err("kv_namespaces[].binding must be string")
}
let id = match o.get("id") {
Some(String(s)) => s
_ => return Err("kv_namespaces[].id must be string")
}
kv_namespaces.push({ binding, id })
}
_ => return Err("kv_namespaces[] must be string or object")
}
}
}
if obj.get("r2_buckets") is Some(Array(arr)) {
for item in arr {
match item {
String(name) =>
r2_buckets.push({
binding: name,
bucket_name: "wite-dev-" + name.to_lower(),
})
Object(o) => {
let binding = match o.get("binding") {
Some(String(s)) => s
_ => return Err("r2_buckets[].binding must be string")
}
let bucket_name = match o.get("bucket_name") {
Some(String(s)) => s
_ => return Err("r2_buckets[].bucket_name must be string")
}
r2_buckets.push({ binding, bucket_name })
}
_ => return Err("r2_buckets[] must be string or object")
}
}
}
if obj.get("d1_databases") is Some(Array(arr)) {
for item in arr {
match item {
String(name) =>
d1_databases.push({
binding: name,
database_name: "wite-dev-" + name.to_lower(),
database_id: "wite-dev-" + name.to_lower(),
})
Object(o) => {
let binding = match o.get("binding") {
Some(String(s)) => s
_ => return Err("d1_databases[].binding must be string")
}
let database_name = match o.get("database_name") {
Some(String(s)) => s
_ => return Err("d1_databases[].database_name must be string")
}
let database_id = match o.get("database_id") {
Some(String(s)) => s
_ => return Err("d1_databases[].database_id must be string")
}
d1_databases.push({ binding, database_name, database_id })
}
_ => return Err("d1_databases[] must be string or object")
}
}
}
Ok({ kv_namespaces, r2_buckets, d1_databases })
}
_ => Err("bindings must be object")
}
}
///|
pub fn parse_server_config(json_value : Json) -> Result[ServerConfig, String] {
match json_value {
Object(obj) => {
let mut executor = ServerExecutor::Wagi
let mut route = "/..."
let mut bindings = empty_server_bindings()
match obj.get("executor") {
Some(String(e)) =>
match e.to_lower() {
"wagi" => executor = ServerExecutor::Wagi
"workerd" => executor = ServerExecutor::Workerd
_ => return Err("invalid server.executor: " + e)
}
Some(_) => return Err("server.executor must be string")
None => ()
}
match obj.get("route") {
Some(String(r)) => route = r
Some(_) => return Err("server.route must be string")
None => ()
}
if obj.get("bindings") is Some(bindings_value) {
match parse_server_bindings(bindings_value) {
Ok(parsed) => bindings = parsed
Err(msg) => return Err(msg)
}
}
Ok({ executor, route, bindings })
}
_ => Err("server must be object")
}
}
///|
pub enum BinaryKind {
Auto
Core
Component
}
///|
pub fn parse_binary_kind(name : String) -> BinaryKind? {
match name.to_lower() {
"auto" => Some(BinaryKind::Auto)
"core" => Some(BinaryKind::Core)
"component" => Some(BinaryKind::Component)
_ => None
}
}
///|
pub fn binary_kind_name(kind : BinaryKind) -> String {
match kind {
BinaryKind::Auto => "auto"
BinaryKind::Core => "core"
BinaryKind::Component => "component"
}
}
///|
pub enum ConfigInput {
None
Single(String)
Named(Map[String, String])
} derive(Debug)
///|
pub impl Show for ConfigInput with fn output(self, logger) {
match self {
None => logger.write_string("None")
Single(s) => {
logger.write_string("Single(")
Show::output(s, logger)
logger.write_string(")")
}
Named(map) => {
logger.write_string("Named(")
write_debug_value(map, logger)
logger.write_string(")")
}
}
}
///|
pub fn ConfigInput::new_none() -> ConfigInput {
ConfigInput::None
}
///|
pub fn ConfigInput::new_single(s : String) -> ConfigInput {
ConfigInput::Single(s)
}
///|
pub fn ConfigInput::new_named(map : Map[String, String]) -> ConfigInput {
ConfigInput::Named(map)
}
///|
pub struct ConfigOutput {
dir : String
}
///|
pub struct ChunkConfig {
build : String?
output : String
wit : String?
world : String?
}
///|
pub fn empty_chunk_config() -> ChunkConfig {
{ build: None, output: "", wit: None, world: None }
}
///|
pub fn parse_chunk_config(
json_value : Json,
label : String,
) -> Result[ChunkConfig, String] {
match json_value {
Object(obj) => {
let mut build : String? = None
let mut output = ""
let mut wit : String? = None
let mut world : String? = None
match obj.get("build") {
Some(String(cmd)) => build = Some(cmd)
Some(_) => return Err(label + ".build must be string")
None => ()
}
match obj.get("output") {
Some(String(path)) => output = path
Some(_) => return Err(label + ".output must be string")
None => return Err(label + ".output is required")
}
match obj.get("wit") {
Some(String(path)) => wit = Some(path)
Some(_) => return Err(label + ".wit must be string")
None => ()
}
match obj.get("world") {
Some(String(name)) => world = Some(name)
Some(_) => return Err(label + ".world must be string")
None => ()
}
Ok({ build, output, wit, world })
}
String(path) => Ok({ build: None, output: path, wit: None, world: None })
_ => Err(label + " must be object or string")
}
}
///|
fn parse_chunks(json_value : Json) -> Result[Map[String, ChunkConfig], String] {
match json_value {
Object(obj) => {
let chunks : Map[String, ChunkConfig] = Map([])
for entry in obj {
match parse_chunk_config(entry.1, "chunks." + entry.0) {
Ok(chunk) => chunks[entry.0] = chunk
Err(msg) => return Err(msg)
}
}
Ok(chunks)
}
_ => Err("chunks must be object")
}
}
///|
pub enum RuntimeTarget {
Browser
Wasmtime
Workerd
Wagi
Custom(String)
}
///|
pub fn parse_runtime_target(name : String) -> RuntimeTarget {
match name.to_lower() {
"browser" => RuntimeTarget::Browser
"wasmtime" => RuntimeTarget::Wasmtime
"workerd" => RuntimeTarget::Workerd
"wagi" => RuntimeTarget::Wagi
_ => RuntimeTarget::Custom(name)
}
}
///|
pub fn runtime_target_name(target : RuntimeTarget) -> String {
match target {
RuntimeTarget::Browser => "browser"
RuntimeTarget::Wasmtime => "wasmtime"
RuntimeTarget::Workerd => "workerd"
RuntimeTarget::Wagi => "wagi"
RuntimeTarget::Custom(name) => name
}
}
///|
pub struct EnvironmentConfig {
input : ConfigInput
output : ConfigOutput
optimize : String?
kind : BinaryKind
server : ServerConfig?
runtime : RuntimeTarget
strip : Bool
closed_world : Bool
closed_world_root_exports : Array[String]
post_commands : Array[String]
}
///|
pub fn empty_environment_config() -> EnvironmentConfig {
{
input: ConfigInput::None,
output: { dir: "dist" },
optimize: None,
kind: BinaryKind::Auto,
server: None,
runtime: RuntimeTarget::Browser,
strip: false,
closed_world: false,
closed_world_root_exports: [],
post_commands: [],
}
}
///|
pub struct EnvironmentDef {
name : String
input : ConfigInput
output : ConfigOutput
optimize : String?
kind : BinaryKind
server : ServerConfig?
runtime : RuntimeTarget
strip : Bool
closed_world : Bool
closed_world_root_exports : Array[String]
post_commands : Array[String]
}
///|
pub struct WiteConfigFile {
build_flags : Array[String]
analyze_flags : Array[String]
profile_flags : Array[String]
build_kind : BinaryKind
analyze_kind : BinaryKind
profile_kind : BinaryKind
input : ConfigInput
output : ConfigOutput
server : ServerConfig?
environments : Map[String, EnvironmentConfig]
chunks : Map[String, ChunkConfig]
}
///|
pub fn empty_wite_config_file() -> WiteConfigFile {
{
build_flags: [],
analyze_flags: [],
profile_flags: [],
build_kind: BinaryKind::Auto,
analyze_kind: BinaryKind::Auto,
profile_kind: BinaryKind::Auto,
input: ConfigInput::None,
output: { dir: "dist" },
server: None,
environments: {},
chunks: {},
}
}
///|
pub fn chunk_names(config : WiteConfigFile) -> Array[String] {
let names : Array[String] = []
config.chunks.each((name, _) => names.push(name))
names
}
///|
pub fn parse_config_input(json_value : Json) -> Result[ConfigInput, String] {
match json_value {
String(s) => s |> ConfigInput::Single |> Ok
Object(obj) => {
let map : Map[String, String] = Map([])
for entry in obj {
match entry.1 {
String(v) => map[entry.0] = v
_ => return Err("config input." + entry.0 + " must be string")
}
}
map |> ConfigInput::Named |> Ok
}
_ => Err("config input must be string or object")
}
}
///|
pub fn parse_config_output(json_value : Json) -> Result[ConfigOutput, String] {
match json_value {
String(s) => Ok({ dir: s })
Object(obj) =>
match obj.get("dir") {
Some(String(d)) => Ok({ dir: d })
Some(_) => Err("config output.dir must be string")
None => Ok({ dir: "dist" })
}
_ => Err("config output must be string or object")
}
}
///|
pub fn config_input_entries(input : ConfigInput) -> Array[(String, String)] {
match input {
ConfigInput::None => []
ConfigInput::Single(s) => [("default", s)]
ConfigInput::Named(map) => {
let out : Array[(String, String)] = []
for entry in map {
out.push((entry.0, entry.1))
}
out
}
}
}
///|
pub fn config_input_single_path(input : ConfigInput) -> String? {
match input {
ConfigInput::None => None
ConfigInput::Single(s) => Some(s)
ConfigInput::Named(map) => {
for entry in map {
return Some(entry.1)
}
None
}
}
}
///|
pub fn parse_json_string_array(
json_value : Json,
label : String,
) -> Result[Array[String], String] {
match json_value {
Array(values) => {
let out : Array[String] = []
for i in 0.. out.push(value)
_ =>
return Err(
"config " + label + "[" + i.to_string() + "] must be string",
)
}
}
Ok(out)
}
_ => Err("config " + label + " must be string array")
}
}
///|
pub fn parse_wite_config_section(
json_value : Json,
label : String,
) -> Result[(Array[String], BinaryKind), String] {
match json_value {
Array(_) =>
match parse_json_string_array(json_value, label) {
Ok(flags) => Ok((flags, BinaryKind::Auto))
Err(msg) => Err(msg)
}
Object(object) => {
let mut flags : Array[String] = []
let mut kind = BinaryKind::Auto
if object.get("flags") is Some(flags_value) {
match parse_json_string_array(flags_value, label + ".flags") {
Ok(parsed) => flags = parsed
Err(msg) => return Err(msg)
}
}
match object.get("kind") {
Some(String(raw_kind)) =>
match parse_binary_kind(raw_kind) {
Some(parsed_kind) => kind = parsed_kind
None => return Err("invalid config " + label + ".kind: " + raw_kind)
}
Some(_) => return Err("config " + label + ".kind must be string")
None => ()
}
Ok((flags, kind))
}
_ => Err("config " + label + " must be array or object")
}
}
///|
pub fn parse_environment_config(
json_value : Json,
label : String,
) -> Result[EnvironmentConfig, String] {
match json_value {
Object(obj) => {
let mut input = ConfigInput::None
let mut output : ConfigOutput = { dir: "dist" }
let mut optimize : String? = None
let mut kind = BinaryKind::Auto
let mut server : ServerConfig? = None
let mut strip = false
let mut closed_world = false
let closed_world_root_exports : Array[String] = []
if obj.get("input") is Some(value) {
match parse_config_input(value) {
Ok(parsed) => input = parsed
Err(msg) => return Err(label + ".input: " + msg)
}
}
if obj.get("output") is Some(value) {
match parse_config_output(value) {
Ok(parsed) => output = parsed
Err(msg) => return Err(label + ".output: " + msg)
}
}
match obj.get("optimize") {
Some(String(level)) => optimize = Some(level)
Some(_) => return Err(label + ".optimize must be string")
None => ()
}
match obj.get("kind") {
Some(String(raw_kind)) =>
match parse_binary_kind(raw_kind) {
Some(parsed) => kind = parsed
None => return Err(label + ".kind: invalid value: " + raw_kind)
}
Some(_) => return Err(label + ".kind must be string")
None => ()
}
if obj.get("server") is Some(value) {
match parse_server_config(value) {
Ok(parsed) => server = Some(parsed)
Err(msg) => return Err(label + ".server: " + msg)
}
}
match obj.get("strip") {
Some(True) => strip = true
Some(False) => strip = false
Some(_) => return Err(label + ".strip must be boolean")
None => ()
}
match obj.get("closed-world") {
Some(True) => closed_world = true
Some(False) => closed_world = false
Some(_) => return Err(label + ".closed-world must be boolean")
None => ()
}
if obj.get("closed-world-roots") is Some(value) {
match parse_json_string_array(value, label + ".closed-world-roots") {
Ok(roots) => closed_world_root_exports.append(roots[:])
Err(msg) => return Err(msg)
}
}
let mut runtime = RuntimeTarget::Browser
match obj.get("runtime") {
Some(String(name)) => runtime = parse_runtime_target(name)
Some(_) => return Err(label + ".runtime must be string")
None => ()
}
let post_commands : Array[String] = []
if obj.get("post") is Some(value) {
match parse_json_string_array(value, label + ".post") {
Ok(cmds) => post_commands.append(cmds[:])
Err(msg) => return Err(msg)
}
}
Ok({
input,
output,
optimize,
kind,
server,
runtime,
strip,
closed_world,
closed_world_root_exports,
post_commands,
})
}
_ => Err(label + " must be object")
}
}
///|
fn parse_environments(
json_value : Json,
) -> Result[Map[String, EnvironmentConfig], String] {
match json_value {
Object(obj) => {
let environments : Map[String, EnvironmentConfig] = Map([])
for entry in obj {
match parse_environment_config(entry.1, "environments." + entry.0) {
Ok(env) => environments[entry.0] = env
Err(msg) => return Err(msg)
}
}
Ok(environments)
}
_ => Err("environments must be object")
}
}
///|
pub fn parse_wite_config_json(
json_value : Json,
) -> Result[WiteConfigFile, String] {
match json_value {
Object(object) => {
let mut build_flags : Array[String] = []
let mut analyze_flags : Array[String] = []
let mut profile_flags : Array[String] = []
let mut build_kind = BinaryKind::Auto
let mut analyze_kind = BinaryKind::Auto
let mut profile_kind = BinaryKind::Auto
if object.get("build") is Some(value) {
match parse_wite_config_section(value, "build") {
Ok((flags, kind)) => {
build_flags = flags
build_kind = kind
}
Err(msg) => return Err(msg)
}
}
if object.get("analyze") is Some(value) {
match parse_wite_config_section(value, "analyze") {
Ok((flags, kind)) => {
analyze_flags = flags
analyze_kind = kind
}
Err(msg) => return Err(msg)
}
}
if object.get("profile") is Some(value) {
match parse_wite_config_section(value, "profile") {
Ok((flags, kind)) => {
profile_flags = flags
profile_kind = kind
}
Err(msg) => return Err(msg)
}
}
let mut input = ConfigInput::None
if object.get("input") is Some(value) {
match parse_config_input(value) {
Ok(parsed) => input = parsed
Err(msg) => return Err(msg)
}
}
let mut output : ConfigOutput = { dir: "dist" }
if object.get("output") is Some(value) {
match parse_config_output(value) {
Ok(parsed) => output = parsed
Err(msg) => return Err(msg)
}
}
let mut server : ServerConfig? = None
if object.get("server") is Some(value) {
match parse_server_config(value) {
Ok(parsed) => server = Some(parsed)
Err(msg) => return Err(msg)
}
}
let mut environments : Map[String, EnvironmentConfig] = Map([])
if object.get("environments") is Some(value) {
match parse_environments(value) {
Ok(parsed) => environments = parsed
Err(msg) => return Err(msg)
}
}
let mut chunks : Map[String, ChunkConfig] = Map([])
if object.get("chunks") is Some(value) {
match parse_chunks(value) {
Ok(parsed) => chunks = parsed
Err(msg) => return Err(msg)
}
}
Ok({
build_flags,
analyze_flags,
profile_flags,
build_kind,
analyze_kind,
profile_kind,
input,
output,
server,
environments,
chunks,
})
}
_ => Err("config root must be object")
}
}
///|
pub fn parse_wite_config_text(text : String) -> Result[WiteConfigFile, String] {
let json_value = @json5.parse(text) catch {
parse_error => return Err(parse_error.to_string())
}
parse_wite_config_json(json_value)
}
///|
pub struct ConfigSelectionFlags {
config_path : String
explicit_config_path : Bool
use_config : Bool
env_name : String?
all_envs : Bool
remaining_flags : Array[String]
}
///|
pub fn parse_config_selection_flags(
flags : Array[String],
) -> Result[ConfigSelectionFlags, String] {
let mut config_path = "wite.config.jsonc"
let mut explicit_config_path = false
let mut use_config = true
let mut env_name : String? = None
let mut all_envs = false
let remaining_flags : Array[String] = []
let mut i = 0
while i < flags.length() {
let flag = flags[i]
if flag == "--no-config" {
use_config = false
i += 1
continue
}
if flag == "--config" {
if i + 1 >= flags.length() {
return Err("missing config path after --config")
}
let path = flags[i + 1]
if path.is_empty() {
return Err("config path must not be empty")
}
config_path = path
explicit_config_path = true
use_config = true
i += 2
continue
}
if flag.strip_prefix("--config=") is Some(path) {
if path.is_empty() {
return Err("config path must not be empty")
}
config_path = path.to_owned()
explicit_config_path = true
use_config = true
i += 1
continue
}
if flag == "--all-envs" {
all_envs = true
i += 1
continue
}
if flag == "--env" {
if i + 1 >= flags.length() {
return Err("missing environment name after --env")
}
let name = flags[i + 1]
if name.is_empty() {
return Err("environment name must not be empty")
}
env_name = Some(name)
i += 2
continue
}
if flag.strip_prefix("--env=") is Some(name) {
if name.is_empty() {
return Err("environment name must not be empty")
}
env_name = Some(name.to_owned())
i += 1
continue
}
remaining_flags.push(flag)
i += 1
}
Ok({
config_path,
explicit_config_path,
use_config,
env_name,
all_envs,
remaining_flags,
})
}
///|
pub fn merge_command_flags(
defaults : Array[String],
cli_flags : Array[String],
) -> Array[String] {
let merged = defaults.copy()
merged.append(cli_flags[:])
merged
}
///|
pub fn should_auto_sync_deps(use_config : Bool, config_exists : Bool) -> Bool {
use_config && config_exists
}
///|
pub fn build_auto_sync_deps_command_args(config_path : String) -> Array[String] {
["wite", "deps", "sync", "--config=" + config_path, "--fail-fast"]
}
///|
pub fn is_ts_config(path : String) -> Bool {
path.has_suffix(".ts")
}
///|
pub fn resolve_environment(
config : WiteConfigFile,
env_name : String?,
) -> Result[EnvironmentConfig, String] {
match env_name {
None =>
Ok({
input: config.input,
output: config.output,
optimize: None,
kind: config.build_kind,
server: config.server,
runtime: RuntimeTarget::Browser,
strip: false,
closed_world: false,
closed_world_root_exports: [],
post_commands: [],
})
Some(name) =>
match config.environments.get(name) {
Some(env) => {
let input = match env.input {
ConfigInput::None => config.input
_ => env.input
}
let output = if env.output.dir == "dist" {
config.output
} else {
env.output
}
let kind = match env.kind {
BinaryKind::Auto => config.build_kind
_ => env.kind
}
let server = match env.server {
Some(_) => env.server
None => config.server
}
Ok({
input,
output,
optimize: env.optimize,
kind,
server,
runtime: env.runtime,
strip: env.strip,
closed_world: env.closed_world,
closed_world_root_exports: env.closed_world_root_exports,
post_commands: env.post_commands,
})
}
None => Err("environment not found: " + name)
}
}
}
///|
pub fn resolve_environment_def(
config : WiteConfigFile,
name : String,
) -> Result[EnvironmentDef, String] {
match resolve_environment(config, Some(name)) {
Ok(env) =>
Ok({
name,
input: env.input,
output: env.output,
optimize: env.optimize,
kind: env.kind,
server: env.server,
runtime: env.runtime,
strip: env.strip,
closed_world: env.closed_world,
closed_world_root_exports: env.closed_world_root_exports,
post_commands: env.post_commands,
})
Err(msg) => Err(msg)
}
}
///|
pub fn resolve_all_environments(
config : WiteConfigFile,
) -> Array[EnvironmentDef] {
let envs : Array[EnvironmentDef] = []
let names = environment_names(config)
if names.length() == 0 {
// No environments defined: create a default one
envs.push({
name: "default",
input: config.input,
output: config.output,
optimize: None,
kind: config.build_kind,
server: config.server,
runtime: RuntimeTarget::Browser,
strip: false,
closed_world: false,
closed_world_root_exports: [],
post_commands: [],
})
return envs
}
for name in names {
match resolve_environment_def(config, name) {
Ok(def) => envs.push(def)
Err(_) => () // skip invalid environments
}
}
envs
}
///|
pub fn environment_names(config : WiteConfigFile) -> Array[String] {
let names : Array[String] = []
config.environments.each((name, _) => names.push(name))
names
}