From de71558c5df94e7cb466acb9e2810ebcfb3028a4 Mon Sep 17 00:00:00 2001
From: Filip Tibell <filip.tibell@gmail.com>
Date: Sun, 12 May 2024 13:30:32 +0200
Subject: [PATCH 1/7] Split lune into proper crates (#188)

---
 .github/workflows/ci.yaml                     |   4 +-
 .github/workflows/publish.yaml                |  24 -
 .github/workflows/release.yaml                |  56 +-
 Cargo.lock                                    | 270 ++++++----
 Cargo.toml                                    | 172 ++-----
 aftman.toml                                   |   6 +-
 crates/lune-roblox/Cargo.toml                 |  27 +
 .../lune-roblox/src}/datatypes/attributes.rs  |  16 +
 .../lune-roblox/src}/datatypes/conversion.rs  |   8 +-
 .../lune-roblox/src}/datatypes/extension.rs   |   1 +
 .../lune-roblox/src}/datatypes/mod.rs         |   2 +-
 .../lune-roblox/src}/datatypes/result.rs      |   0
 .../lune-roblox/src}/datatypes/types/axes.rs  |   7 +-
 .../src}/datatypes/types/brick_color.rs       |   6 +-
 .../src}/datatypes/types/cframe.rs            |  48 +-
 .../src}/datatypes/types/color3.rs            |  10 +-
 .../src}/datatypes/types/color_sequence.rs    |  14 +-
 .../types/color_sequence_keypoint.rs          |   6 +-
 .../lune-roblox/src}/datatypes/types/enum.rs  |   0
 .../src}/datatypes/types/enum_item.rs         |   2 +-
 .../lune-roblox/src}/datatypes/types/enums.rs |   3 +-
 .../lune-roblox/src}/datatypes/types/faces.rs |   9 +-
 .../lune-roblox/src}/datatypes/types/font.rs  |  34 +-
 .../lune-roblox/src}/datatypes/types/mod.rs   |   0
 .../src}/datatypes/types/number_range.rs      |   6 +-
 .../src}/datatypes/types/number_sequence.rs   |  14 +-
 .../types/number_sequence_keypoint.rs         |   6 +-
 .../datatypes/types/physical_properties.rs    |   6 +-
 .../lune-roblox/src}/datatypes/types/ray.rs   |   4 +-
 .../lune-roblox/src}/datatypes/types/rect.rs  |   4 +-
 .../src}/datatypes/types/region3.rs           |   4 +-
 .../src}/datatypes/types/region3int16.rs      |   4 +-
 .../lune-roblox/src}/datatypes/types/udim.rs  |   6 +-
 .../lune-roblox/src}/datatypes/types/udim2.rs |   8 +-
 .../src}/datatypes/types/vector2.rs           |   4 +-
 .../src}/datatypes/types/vector2int16.rs      |   4 +-
 .../src}/datatypes/types/vector3.rs           |  13 +-
 .../src}/datatypes/types/vector3int16.rs      |   4 +-
 .../lune-roblox/src}/datatypes/util.rs        |   0
 .../lune-roblox/src}/document/error.rs        |   0
 .../lune-roblox/src}/document/format.rs       |   0
 .../lune-roblox/src}/document/kind.rs         |   3 +-
 .../lune-roblox/src}/document/mod.rs          |  42 +-
 .../src}/document/postprocessing.rs           |   2 +-
 .../lune-roblox/src}/exports.rs               |   0
 .../lune-roblox/src}/instance/base.rs         |  28 +-
 .../lune-roblox/src}/instance/data_model.rs   |  12 +-
 .../lune-roblox/src}/instance/mod.rs          |  46 +-
 .../lune-roblox/src}/instance/registry.rs     |  49 ++
 .../lune-roblox/src}/instance/terrain.rs      |   4 +-
 .../lune-roblox/src}/instance/workspace.rs    |   4 +-
 .../mod.rs => crates/lune-roblox/src/lib.rs   |  14 +-
 .../lune-roblox/src}/reflection/class.rs      |  19 +-
 .../lune-roblox/src}/reflection/enums.rs      |   6 +-
 .../lune-roblox/src}/reflection/mod.rs        |  19 +-
 .../lune-roblox/src}/reflection/property.rs   |   8 +-
 .../lune-roblox/src}/reflection/utils.rs      |   8 +-
 .../lune-roblox/src}/shared/classes.rs        |  16 +-
 .../lune-roblox/src}/shared/instance.rs       |   6 +-
 .../lune-roblox/src}/shared/mod.rs            |   0
 .../lune-roblox/src}/shared/userdata.rs       |  16 +-
 crates/lune-std-datetime/Cargo.toml           |  20 +
 .../lune-std-datetime/src/date_time.rs        |  49 +-
 crates/lune-std-datetime/src/lib.rs           |  36 ++
 .../lune-std-datetime/src/result.rs           |   0
 .../lune-std-datetime/src}/values.rs          |  12 +-
 crates/lune-std-fs/Cargo.toml                 |  21 +
 .../fs => crates/lune-std-fs/src}/copy.rs     |  10 +-
 .../mod.rs => crates/lune-std-fs/src/lib.rs   |  19 +-
 .../fs => crates/lune-std-fs/src}/metadata.rs |   2 +-
 .../fs => crates/lune-std-fs/src}/options.rs  |   0
 crates/lune-std-luau/Cargo.toml               |  16 +
 .../mod.rs => crates/lune-std-luau/src/lib.rs |  16 +-
 .../lune-std-luau/src}/options.rs             |  10 +
 crates/lune-std-net/Cargo.toml                |  37 ++
 .../net => crates/lune-std-net/src}/client.rs |   8 +-
 .../net => crates/lune-std-net/src}/config.rs |   2 +-
 .../mod.rs => crates/lune-std-net/src/lib.rs  |  22 +-
 .../lune-std-net/src}/server/keys.rs          |   0
 .../lune-std-net/src}/server/mod.rs           |   8 +-
 .../lune-std-net/src}/server/request.rs       |   2 +-
 .../lune-std-net/src}/server/response.rs      |   0
 .../lune-std-net/src}/server/service.rs       |   0
 .../net => crates/lune-std-net/src}/util.rs   |   4 +-
 .../lune-std-net/src}/websocket.rs            |   6 +-
 crates/lune-std-process/Cargo.toml            |  26 +
 .../lune-std-process/src/lib.rs               | 101 ++--
 .../lune-std-process/src}/options/kind.rs     |   2 +-
 .../lune-std-process/src}/options/mod.rs      |   2 +-
 .../lune-std-process/src}/options/stdio.rs    |   0
 .../lune-std-process/src}/tee_writer.rs       |   0
 .../lune-std-process/src}/wait_for_child.rs   |   3 +-
 crates/lune-std-regex/Cargo.toml              |  19 +
 .../lune-std-regex/src}/captures.rs           |   0
 .../lune-std-regex/src/lib.rs                 |  13 +-
 .../lune-std-regex/src}/matches.rs            |   0
 .../lune-std-regex/src}/regex.rs              |   2 +-
 crates/lune-std-roblox/Cargo.toml             |  21 +
 .../lune-std-roblox/src/lib.rs                |  46 +-
 crates/lune-std-serde/Cargo.toml              |  35 ++
 .../src}/compress_decompress.rs               |  91 +++-
 crates/lune-std-serde/src/encode_decode.rs    | 158 ++++++
 crates/lune-std-serde/src/lib.rs              |  57 +++
 crates/lune-std-stdio/Cargo.toml              |  23 +
 crates/lune-std-stdio/src/lib.rs              |  85 ++++
 .../lune-std-stdio/src}/prompt.rs             | 123 +++--
 crates/lune-std-stdio/src/style_and_color.rs  | 195 +++++++
 crates/lune-std-task/Cargo.toml               |  19 +
 .../mod.rs => crates/lune-std-task/src/lib.rs |  31 +-
 crates/lune-std/Cargo.toml                    |  57 +++
 crates/lune-std/src/global.rs                 |  92 ++++
 crates/lune-std/src/globals/g_table.rs        |   5 +
 crates/lune-std/src/globals/mod.rs            |   5 +
 crates/lune-std/src/globals/print.rs          |  19 +
 .../lune-std/src}/globals/require/alias.rs    |  14 +-
 .../lune-std/src}/globals/require/context.rs  |  60 ++-
 .../lune-std/src/globals/require/library.rs   |   4 +-
 .../lune-std/src}/globals/require/mod.rs      |  22 +-
 .../lune-std/src}/globals/require/path.rs     |   2 +-
 crates/lune-std/src/globals/version.rs        |  35 ++
 crates/lune-std/src/globals/warn.rs           |  23 +
 crates/lune-std/src/lib.rs                    |  29 ++
 crates/lune-std/src/library.rs                | 127 +++++
 .../util => crates/lune-std/src}/luaurc.rs    |  65 ++-
 crates/lune-utils/Cargo.toml                  |  22 +
 crates/lune-utils/src/fmt/error/components.rs | 152 ++++++
 crates/lune-utils/src/fmt/error/mod.rs        |   8 +
 .../lune-utils/src/fmt/error/stack_trace.rs   | 170 +++++++
 crates/lune-utils/src/fmt/error/tests.rs      |  85 ++++
 crates/lune-utils/src/fmt/label.rs            |  66 +++
 crates/lune-utils/src/fmt/mod.rs              |   7 +
 crates/lune-utils/src/fmt/value/basic.rs      |  74 +++
 crates/lune-utils/src/fmt/value/config.rs     |  48 ++
 .../lune-utils/src/fmt/value/metamethods.rs   |  29 ++
 crates/lune-utils/src/fmt/value/mod.rs        |  65 +++
 crates/lune-utils/src/fmt/value/recursive.rs  |  89 ++++
 crates/lune-utils/src/fmt/value/style.rs      |   9 +
 crates/lune-utils/src/lib.rs                  |  10 +
 crates/lune-utils/src/path.rs                 | 101 ++++
 .../lune-utils/src}/table_builder.rs          |  79 ++-
 crates/lune-utils/src/version_string.rs       |  75 +++
 crates/lune/Cargo.toml                        |  89 ++++
 .../lune/src}/cli/build/base_exe.rs           |   0
 {src => crates/lune/src}/cli/build/files.rs   |   0
 {src => crates/lune/src}/cli/build/mod.rs     |   0
 {src => crates/lune/src}/cli/build/result.rs  |   0
 {src => crates/lune/src}/cli/build/target.rs  |   4 +-
 {src => crates/lune/src}/cli/list.rs          |   0
 {src => crates/lune/src}/cli/mod.rs           |   0
 {src => crates/lune/src}/cli/repl.rs          |   0
 {src => crates/lune/src}/cli/run.rs           |   0
 {src => crates/lune/src}/cli/setup.rs         |   0
 {src => crates/lune/src}/cli/utils/files.rs   |   8 +-
 {src => crates/lune/src}/cli/utils/listing.rs |   0
 {src => crates/lune/src}/cli/utils/mod.rs     |   0
 crates/lune/src/lib.rs                        |  11 +
 crates/lune/src/main.rs                       |  42 ++
 crates/lune/src/rt/mod.rs                     |   5 +
 .../error.rs => crates/lune/src/rt/result.rs  |  13 +-
 .../mod.rs => crates/lune/src/rt/runtime.rs   |  36 +-
 .../lune/src}/standalone/metadata.rs          |   2 +-
 {src => crates/lune/src}/standalone/mod.rs    |   0
 {src => crates/lune/src}/standalone/tracer.rs |   0
 {src => crates/lune/src}/tests.rs             | 190 ++++---
 scripts/generate_compression_test_files.luau  | 303 +++++++++++
 src/lib.rs                                    |   9 -
 src/lune/builtins/mod.rs                      |  92 ----
 src/lune/builtins/serde/encode_decode.rs      | 131 -----
 src/lune/builtins/serde/mod.rs                |  48 --
 src/lune/builtins/stdio/mod.rs                | 126 -----
 src/lune/globals/g_table.rs                   |   5 -
 src/lune/globals/mod.rs                       |  26 -
 src/lune/globals/print.rs                     |  15 -
 src/lune/globals/version.rs                   |  39 --
 src/lune/globals/warn.rs                      |  19 -
 src/lune/util/formatting.rs                   | 477 ------------------
 src/lune/util/mod.rs                          |   8 -
 src/lune/util/paths.rs                        |  21 -
 src/lune/util/traits.rs                       |  15 -
 src/main.rs                                   |  47 --
 tests/net/serve/requests.luau                 |   2 +-
 tests/require/tests/state_module.luau         |   2 +-
 tests/serde/test-files/loremipsum.txt.gz      | Bin 140 -> 140 bytes
 tests/serde/test-files/loremipsum.txt.lz4     | Bin 170 -> 187 bytes
 tests/serde/test-files/loremipsum.txt.z       |   3 +-
 185 files changed, 4005 insertions(+), 1897 deletions(-)
 delete mode 100644 .github/workflows/publish.yaml
 create mode 100644 crates/lune-roblox/Cargo.toml
 rename {src/roblox => crates/lune-roblox/src}/datatypes/attributes.rs (81%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/conversion.rs (98%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/extension.rs (97%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/mod.rs (82%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/result.rs (100%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/axes.rs (95%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/brick_color.rs (99%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/cframe.rs (92%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/color3.rs (97%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/color_sequence.rs (93%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/color_sequence_keypoint.rs (94%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/enum.rs (100%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/enum_item.rs (98%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/enums.rs (94%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/faces.rs (95%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/font.rs (96%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/mod.rs (100%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/number_range.rs (94%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/number_sequence.rs (93%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/number_sequence_keypoint.rs (94%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/physical_properties.rs (98%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/ray.rs (97%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/rect.rs (98%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/region3.rs (97%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/region3int16.rs (96%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/udim.rs (96%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/udim2.rs (97%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/vector2.rs (98%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/vector2int16.rs (98%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/vector3.rs (95%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/types/vector3int16.rs (98%)
 rename {src/roblox => crates/lune-roblox/src}/datatypes/util.rs (100%)
 rename {src/roblox => crates/lune-roblox/src}/document/error.rs (100%)
 rename {src/roblox => crates/lune-roblox/src}/document/format.rs (100%)
 rename {src/roblox => crates/lune-roblox/src}/document/kind.rs (98%)
 rename {src/roblox => crates/lune-roblox/src}/document/mod.rs (91%)
 rename {src/roblox => crates/lune-roblox/src}/document/postprocessing.rs (96%)
 rename {src/roblox => crates/lune-roblox/src}/exports.rs (100%)
 rename {src/roblox => crates/lune-roblox/src}/instance/base.rs (93%)
 rename {src/roblox => crates/lune-roblox/src}/instance/data_model.rs (89%)
 rename {src/roblox => crates/lune-roblox/src}/instance/mod.rs (96%)
 rename {src/roblox => crates/lune-roblox/src}/instance/registry.rs (86%)
 rename {src/roblox => crates/lune-roblox/src}/instance/terrain.rs (99%)
 rename {src/roblox => crates/lune-roblox/src}/instance/workspace.rs (90%)
 rename src/roblox/mod.rs => crates/lune-roblox/src/lib.rs (81%)
 rename {src/roblox => crates/lune-roblox/src}/reflection/class.rs (91%)
 rename {src/roblox => crates/lune-roblox/src}/reflection/enums.rs (91%)
 rename {src/roblox => crates/lune-roblox/src}/reflection/mod.rs (89%)
 rename {src/roblox => crates/lune-roblox/src}/reflection/property.rs (93%)
 rename {src/roblox => crates/lune-roblox/src}/reflection/utils.rs (86%)
 rename {src/roblox => crates/lune-roblox/src}/shared/classes.rs (89%)
 rename {src/roblox => crates/lune-roblox/src}/shared/instance.rs (98%)
 rename {src/roblox => crates/lune-roblox/src}/shared/mod.rs (100%)
 rename {src/roblox => crates/lune-roblox/src}/shared/userdata.rs (91%)
 create mode 100644 crates/lune-std-datetime/Cargo.toml
 rename src/lune/builtins/datetime/mod.rs => crates/lune-std-datetime/src/date_time.rs (91%)
 create mode 100644 crates/lune-std-datetime/src/lib.rs
 rename src/lune/builtins/datetime/error.rs => crates/lune-std-datetime/src/result.rs (100%)
 rename {src/lune/builtins/datetime => crates/lune-std-datetime/src}/values.rs (92%)
 create mode 100644 crates/lune-std-fs/Cargo.toml
 rename {src/lune/builtins/fs => crates/lune-std-fs/src}/copy.rs (96%)
 rename src/lune/builtins/fs/mod.rs => crates/lune-std-fs/src/lib.rs (92%)
 rename {src/lune/builtins/fs => crates/lune-std-fs/src}/metadata.rs (98%)
 rename {src/lune/builtins/fs => crates/lune-std-fs/src}/options.rs (100%)
 create mode 100644 crates/lune-std-luau/Cargo.toml
 rename src/lune/builtins/luau/mod.rs => crates/lune-std-luau/src/lib.rs (85%)
 rename {src/lune/builtins/luau => crates/lune-std-luau/src}/options.rs (95%)
 create mode 100644 crates/lune-std-net/Cargo.toml
 rename {src/lune/builtins/net => crates/lune-std-net/src}/client.rs (96%)
 rename {src/lune/builtins/net => crates/lune-std-net/src}/config.rs (99%)
 rename src/lune/builtins/net/mod.rs => crates/lune-std-net/src/lib.rs (83%)
 rename {src/lune/builtins/net => crates/lune-std-net/src}/server/keys.rs (100%)
 rename {src/lune/builtins/net => crates/lune-std-net/src}/server/mod.rs (94%)
 rename {src/lune/builtins/net => crates/lune-std-net/src}/server/request.rs (97%)
 rename {src/lune/builtins/net => crates/lune-std-net/src}/server/response.rs (100%)
 rename {src/lune/builtins/net => crates/lune-std-net/src}/server/service.rs (100%)
 rename {src/lune/builtins/net => crates/lune-std-net/src}/util.rs (97%)
 rename {src/lune/builtins/net => crates/lune-std-net/src}/websocket.rs (96%)
 create mode 100644 crates/lune-std-process/Cargo.toml
 rename src/lune/builtins/process/mod.rs => crates/lune-std-process/src/lib.rs (75%)
 rename {src/lune/builtins/process => crates/lune-std-process/src}/options/kind.rs (98%)
 rename {src/lune/builtins/process => crates/lune-std-process/src}/options/mod.rs (98%)
 rename {src/lune/builtins/process => crates/lune-std-process/src}/options/stdio.rs (100%)
 rename {src/lune/builtins/process => crates/lune-std-process/src}/tee_writer.rs (100%)
 rename {src/lune/builtins/process => crates/lune-std-process/src}/wait_for_child.rs (94%)
 create mode 100644 crates/lune-std-regex/Cargo.toml
 rename {src/lune/builtins/regex => crates/lune-std-regex/src}/captures.rs (100%)
 rename src/lune/builtins/regex/mod.rs => crates/lune-std-regex/src/lib.rs (56%)
 rename {src/lune/builtins/regex => crates/lune-std-regex/src}/matches.rs (100%)
 rename {src/lune/builtins/regex => crates/lune-std-regex/src}/regex.rs (98%)
 create mode 100644 crates/lune-std-roblox/Cargo.toml
 rename src/lune/builtins/roblox/mod.rs => crates/lune-std-roblox/src/lib.rs (83%)
 create mode 100644 crates/lune-std-serde/Cargo.toml
 rename {src/lune/builtins/serde => crates/lune-std-serde/src}/compress_decompress.rs (66%)
 create mode 100644 crates/lune-std-serde/src/encode_decode.rs
 create mode 100644 crates/lune-std-serde/src/lib.rs
 create mode 100644 crates/lune-std-stdio/Cargo.toml
 create mode 100644 crates/lune-std-stdio/src/lib.rs
 rename {src/lune/builtins/stdio => crates/lune-std-stdio/src}/prompt.rs (60%)
 create mode 100644 crates/lune-std-stdio/src/style_and_color.rs
 create mode 100644 crates/lune-std-task/Cargo.toml
 rename src/lune/builtins/task/mod.rs => crates/lune-std-task/src/lib.rs (81%)
 create mode 100644 crates/lune-std/Cargo.toml
 create mode 100644 crates/lune-std/src/global.rs
 create mode 100644 crates/lune-std/src/globals/g_table.rs
 create mode 100644 crates/lune-std/src/globals/mod.rs
 create mode 100644 crates/lune-std/src/globals/print.rs
 rename {src/lune => crates/lune-std/src}/globals/require/alias.rs (88%)
 rename {src/lune => crates/lune-std/src}/globals/require/context.rs (86%)
 rename src/lune/globals/require/builtin.rs => crates/lune-std/src/globals/require/library.rs (70%)
 rename {src/lune => crates/lune-std/src}/globals/require/mod.rs (85%)
 rename {src/lune => crates/lune-std/src}/globals/require/path.rs (97%)
 create mode 100644 crates/lune-std/src/globals/version.rs
 create mode 100644 crates/lune-std/src/globals/warn.rs
 create mode 100644 crates/lune-std/src/lib.rs
 create mode 100644 crates/lune-std/src/library.rs
 rename {src/lune/util => crates/lune-std/src}/luaurc.rs (65%)
 create mode 100644 crates/lune-utils/Cargo.toml
 create mode 100644 crates/lune-utils/src/fmt/error/components.rs
 create mode 100644 crates/lune-utils/src/fmt/error/mod.rs
 create mode 100644 crates/lune-utils/src/fmt/error/stack_trace.rs
 create mode 100644 crates/lune-utils/src/fmt/error/tests.rs
 create mode 100644 crates/lune-utils/src/fmt/label.rs
 create mode 100644 crates/lune-utils/src/fmt/mod.rs
 create mode 100644 crates/lune-utils/src/fmt/value/basic.rs
 create mode 100644 crates/lune-utils/src/fmt/value/config.rs
 create mode 100644 crates/lune-utils/src/fmt/value/metamethods.rs
 create mode 100644 crates/lune-utils/src/fmt/value/mod.rs
 create mode 100644 crates/lune-utils/src/fmt/value/recursive.rs
 create mode 100644 crates/lune-utils/src/fmt/value/style.rs
 create mode 100644 crates/lune-utils/src/lib.rs
 create mode 100644 crates/lune-utils/src/path.rs
 rename {src/lune/util => crates/lune-utils/src}/table_builder.rs (61%)
 create mode 100644 crates/lune-utils/src/version_string.rs
 create mode 100644 crates/lune/Cargo.toml
 rename {src => crates/lune/src}/cli/build/base_exe.rs (100%)
 rename {src => crates/lune/src}/cli/build/files.rs (100%)
 rename {src => crates/lune/src}/cli/build/mod.rs (100%)
 rename {src => crates/lune/src}/cli/build/result.rs (100%)
 rename {src => crates/lune/src}/cli/build/target.rs (96%)
 rename {src => crates/lune/src}/cli/list.rs (100%)
 rename {src => crates/lune/src}/cli/mod.rs (100%)
 rename {src => crates/lune/src}/cli/repl.rs (100%)
 rename {src => crates/lune/src}/cli/run.rs (100%)
 rename {src => crates/lune/src}/cli/setup.rs (100%)
 rename {src => crates/lune/src}/cli/utils/files.rs (96%)
 rename {src => crates/lune/src}/cli/utils/listing.rs (100%)
 rename {src => crates/lune/src}/cli/utils/mod.rs (100%)
 create mode 100644 crates/lune/src/lib.rs
 create mode 100644 crates/lune/src/main.rs
 create mode 100644 crates/lune/src/rt/mod.rs
 rename src/lune/error.rs => crates/lune/src/rt/result.rs (89%)
 rename src/lune/mod.rs => crates/lune/src/rt/runtime.rs (73%)
 rename {src => crates/lune/src}/standalone/metadata.rs (98%)
 rename {src => crates/lune/src}/standalone/mod.rs (100%)
 rename {src => crates/lune/src}/standalone/tracer.rs (100%)
 rename {src => crates/lune/src}/tests.rs (86%)
 create mode 100644 scripts/generate_compression_test_files.luau
 delete mode 100644 src/lib.rs
 delete mode 100644 src/lune/builtins/mod.rs
 delete mode 100644 src/lune/builtins/serde/encode_decode.rs
 delete mode 100644 src/lune/builtins/serde/mod.rs
 delete mode 100644 src/lune/builtins/stdio/mod.rs
 delete mode 100644 src/lune/globals/g_table.rs
 delete mode 100644 src/lune/globals/mod.rs
 delete mode 100644 src/lune/globals/print.rs
 delete mode 100644 src/lune/globals/version.rs
 delete mode 100644 src/lune/globals/warn.rs
 delete mode 100644 src/lune/util/formatting.rs
 delete mode 100644 src/lune/util/mod.rs
 delete mode 100644 src/lune/util/paths.rs
 delete mode 100644 src/lune/util/traits.rs
 delete mode 100644 src/main.rs

diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index b60e128..c5ebff1 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -24,7 +24,7 @@ jobs:
           components: rustfmt
 
       - name: Install Just
-        uses: extractions/setup-just@v1
+        uses: extractions/setup-just@v2
 
       - name: Install Tooling
         uses: ok-nick/setup-aftman@v0.4.2
@@ -41,7 +41,7 @@ jobs:
         uses: actions/checkout@v4
 
       - name: Install Just
-        uses: extractions/setup-just@v1
+        uses: extractions/setup-just@v2
 
       - name: Install Tooling
         uses: ok-nick/setup-aftman@v0.4.2
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
deleted file mode 100644
index 13fc2be..0000000
--- a/.github/workflows/publish.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-name: Publish
-
-on:
-  push:
-    branches:
-      - "main"
-  workflow_dispatch:
-
-jobs:
-  publish:
-    name: Publish
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout repository
-        uses: actions/checkout@v4
-
-      - name: Install Rust
-        uses: dtolnay/rust-toolchain@stable
-
-      - name: Publish to crates.io
-        uses: katyo/publish-crates@v2
-        with:
-          registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
-          ignore-unpublished-changes: true
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 85b25f1..12f0e91 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -21,14 +21,32 @@ jobs:
         uses: actions/checkout@v4
 
       - name: Get version from manifest
-        uses: SebRollen/toml-action@9062fbef52816d61278d24ce53c8070440e1e8dd
+        uses: SebRollen/toml-action@v1.2.0
         id: get_version
         with:
           file: Cargo.toml
           field: package.version
 
-  build:
+  dry-run:
+    name: Dry-run
     needs: ["init"]
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@stable
+
+      - name: Publish (dry-run)
+        uses: katyo/publish-crates@v2
+        with:
+          dry-run: true
+          check-repo: true
+          registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
+
+  build:
+    needs: ["init", "dry-run"]
     strategy:
       fail-fast: false
       matrix:
@@ -70,7 +88,7 @@ jobs:
           targets: ${{ matrix.cargo-target }}
 
       - name: Install Just
-        uses: extractions/setup-just@v1
+        uses: extractions/setup-just@v2
 
       - name: Install build tooling (aarch64-unknown-linux-gnu)
         if: matrix.cargo-target == 'aarch64-unknown-linux-gnu'
@@ -86,24 +104,24 @@ jobs:
         run: just zip-release ${{ matrix.cargo-target }}
 
       - name: Upload release artifact
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: ${{ matrix.artifact-name }}
           path: release.zip
 
-  release:
-    name: Release
+  release-github:
+    name: Release (GitHub)
     runs-on: ubuntu-latest
-    needs: ["init", "build"]
+    needs: ["init", "dry-run", "build"]
     steps:
       - name: Checkout repository
         uses: actions/checkout@v4
 
       - name: Install Just
-        uses: extractions/setup-just@v1
+        uses: extractions/setup-just@v2
 
       - name: Download releases
-        uses: actions/download-artifact@v3
+        uses: actions/download-artifact@v4
         with:
           path: ./releases
 
@@ -111,7 +129,7 @@ jobs:
         run: just unpack-releases "./releases"
 
       - name: Create release
-        uses: softprops/action-gh-release@v1
+        uses: softprops/action-gh-release@v2
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         with:
@@ -120,3 +138,21 @@ jobs:
           fail_on_unmatched_files: true
           files: ./releases/*.zip
           draft: true
+
+  release-crates:
+    name: Release (crates.io)
+    runs-on: ubuntu-latest
+    needs: ["init", "dry-run", "build"]
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+
+      - name: Install Rust
+        uses: dtolnay/rust-toolchain@stable
+
+      - name: Publish crates
+        uses: katyo/publish-crates@v2
+        with:
+          dry-run: false
+          check-repo: true
+          registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}
diff --git a/Cargo.lock b/Cargo.lock
index 2bc2755..b4455c2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -163,9 +163,9 @@ dependencies = [
 
 [[package]]
 name = "async-compression"
-version = "0.4.8"
+version = "0.4.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60"
+checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498"
 dependencies = [
  "brotli",
  "flate2",
@@ -205,17 +205,6 @@ version = "4.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
 
-[[package]]
-name = "async-trait"
-version = "0.1.80"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.60",
-]
-
 [[package]]
 name = "atomic-waker"
 version = "1.1.2"
@@ -324,9 +313,9 @@ dependencies = [
 
 [[package]]
 name = "brotli"
-version = "4.0.0"
+version = "6.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569"
+checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
 dependencies = [
  "alloc-no-stdlib",
  "alloc-stdlib",
@@ -335,9 +324,9 @@ dependencies = [
 
 [[package]]
 name = "brotli-decompressor"
-version = "3.0.0"
+version = "4.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525"
+checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76"
 dependencies = [
  "alloc-no-stdlib",
  "alloc-stdlib",
@@ -759,12 +748,6 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b"
 
-[[package]]
-name = "either"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
-
 [[package]]
 name = "encode_unicode"
 version = "0.3.6"
@@ -1367,15 +1350,6 @@ version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
 
-[[package]]
-name = "itertools"
-version = "0.12.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
-dependencies = [
- "either",
-]
-
 [[package]]
 name = "itoa"
 version = "1.0.11"
@@ -1484,57 +1458,203 @@ name = "lune"
 version = "0.8.3"
 dependencies = [
  "anyhow",
- "async-compression",
- "async-trait",
- "blocking",
- "bstr",
- "chrono",
- "chrono_lc",
  "clap",
  "console",
  "dialoguer",
  "directories",
- "dunce",
  "env_logger",
  "futures-util",
+ "include_dir",
+ "lune-roblox",
+ "lune-std",
+ "lune-utils",
+ "mlua",
+ "mlua-luau-scheduler",
+ "once_cell",
+ "reqwest",
+ "rustyline",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+ "zip_next",
+]
+
+[[package]]
+name = "lune-roblox"
+version = "0.1.0"
+dependencies = [
  "glam",
+ "lune-utils",
+ "mlua",
+ "once_cell",
+ "rand",
+ "rbx_binary",
+ "rbx_dom_weak",
+ "rbx_reflection",
+ "rbx_reflection_database",
+ "rbx_xml",
+ "thiserror",
+]
+
+[[package]]
+name = "lune-std"
+version = "0.1.0"
+dependencies = [
+ "lune-std-datetime",
+ "lune-std-fs",
+ "lune-std-luau",
+ "lune-std-net",
+ "lune-std-process",
+ "lune-std-regex",
+ "lune-std-roblox",
+ "lune-std-serde",
+ "lune-std-stdio",
+ "lune-std-task",
+ "lune-utils",
+ "mlua",
+ "mlua-luau-scheduler",
+ "serde",
+ "serde_json",
+ "tokio",
+]
+
+[[package]]
+name = "lune-std-datetime"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "chrono_lc",
+ "lune-utils",
+ "mlua",
+ "thiserror",
+]
+
+[[package]]
+name = "lune-std-fs"
+version = "0.1.0"
+dependencies = [
+ "bstr",
+ "lune-std-datetime",
+ "lune-utils",
+ "mlua",
+ "tokio",
+]
+
+[[package]]
+name = "lune-std-luau"
+version = "0.1.0"
+dependencies = [
+ "lune-utils",
+ "mlua",
+]
+
+[[package]]
+name = "lune-std-net"
+version = "0.1.0"
+dependencies = [
+ "bstr",
+ "futures-util",
  "http 1.1.0",
  "http-body-util",
  "hyper 1.3.1",
  "hyper-tungstenite",
  "hyper-util",
- "include_dir",
- "itertools",
- "lz4_flex",
+ "lune-std-serde",
+ "lune-utils",
+ "mlua",
+ "mlua-luau-scheduler",
+ "reqwest",
+ "tokio",
+ "tokio-tungstenite",
+ "urlencoding",
+]
+
+[[package]]
+name = "lune-std-process"
+version = "0.1.0"
+dependencies = [
+ "directories",
+ "lune-utils",
+ "mlua",
+ "mlua-luau-scheduler",
+ "os_str_bytes",
+ "pin-project",
+ "tokio",
+]
+
+[[package]]
+name = "lune-std-regex"
+version = "0.1.0"
+dependencies = [
+ "lune-utils",
+ "mlua",
+ "regex",
+ "self_cell",
+]
+
+[[package]]
+name = "lune-std-roblox"
+version = "0.1.0"
+dependencies = [
+ "lune-roblox",
+ "lune-utils",
  "mlua",
  "mlua-luau-scheduler",
  "once_cell",
- "os_str_bytes",
- "path-clean",
- "pathdiff",
- "pin-project",
- "rand",
- "rbx_binary",
  "rbx_cookie",
- "rbx_dom_weak",
- "rbx_reflection",
- "rbx_reflection_database",
- "rbx_xml",
- "regex",
- "reqwest",
- "rustyline",
- "self_cell",
+]
+
+[[package]]
+name = "lune-std-serde"
+version = "0.1.0"
+dependencies = [
+ "async-compression",
+ "bstr",
+ "lune-utils",
+ "lz4",
+ "mlua",
  "serde",
  "serde_json",
  "serde_yaml",
- "thiserror",
  "tokio",
- "tokio-tungstenite",
  "toml",
- "tracing",
- "tracing-subscriber",
- "urlencoding",
- "zip_next",
+]
+
+[[package]]
+name = "lune-std-stdio"
+version = "0.1.0"
+dependencies = [
+ "dialoguer",
+ "lune-utils",
+ "mlua",
+ "mlua-luau-scheduler",
+ "tokio",
+]
+
+[[package]]
+name = "lune-std-task"
+version = "0.1.0"
+dependencies = [
+ "lune-utils",
+ "mlua",
+ "mlua-luau-scheduler",
+ "tokio",
+]
+
+[[package]]
+name = "lune-utils"
+version = "0.1.0"
+dependencies = [
+ "console",
+ "dunce",
+ "mlua",
+ "once_cell",
+ "path-clean",
+ "pathdiff",
+ "tokio",
 ]
 
 [[package]]
@@ -1557,15 +1677,6 @@ dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "lz4_flex"
-version = "0.11.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5"
-dependencies = [
- "twox-hash",
-]
-
 [[package]]
 name = "lzma-rs"
 version = "0.3.0"
@@ -2624,12 +2735,6 @@ dependencies = [
  "version_check",
 ]
 
-[[package]]
-name = "static_assertions"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
-
 [[package]]
 name = "stdweb"
 version = "0.4.20"
@@ -2882,7 +2987,6 @@ dependencies = [
  "signal-hook-registry",
  "socket2",
  "tokio-macros",
- "tracing",
  "windows-sys 0.48.0",
 ]
 
@@ -3100,16 +3204,6 @@ dependencies = [
  "utf-8",
 ]
 
-[[package]]
-name = "twox-hash"
-version = "1.6.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
-dependencies = [
- "cfg-if",
- "static_assertions",
-]
-
 [[package]]
 name = "typed-arena"
 version = "2.0.2"
diff --git a/Cargo.toml b/Cargo.toml
index 8199082..904de54 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,41 +1,21 @@
-[package]
-name = "lune"
-version = "0.8.3"
-edition = "2021"
-license = "MPL-2.0"
-repository = "https://github.com/lune-org/lune"
-description = "A standalone Luau runtime"
-readme = "README.md"
-keywords = ["cli", "lua", "luau", "runtime"]
-categories = ["command-line-interface"]
-
-[[bin]]
-name = "lune"
-path = "src/main.rs"
-
-[lib]
-name = "lune"
-path = "src/lib.rs"
-
-[features]
-default = ["cli", "roblox"]
-cli = [
-    "dep:anyhow",
-    "dep:env_logger",
-    "dep:clap",
-    "dep:include_dir",
-    "dep:rustyline",
-    "dep:zip_next",
-]
-roblox = [
-    "dep:glam",
-    "dep:rand",
-    "dep:rbx_cookie",
-    "dep:rbx_binary",
-    "dep:rbx_dom_weak",
-    "dep:rbx_reflection",
-    "dep:rbx_reflection_database",
-    "dep:rbx_xml",
+[workspace]
+resolver = "2"
+default-members = ["crates/lune"]
+members = [
+    "crates/lune",
+    "crates/lune-roblox",
+    "crates/lune-std",
+    "crates/lune-std-datetime",
+    "crates/lune-std-fs",
+    "crates/lune-std-luau",
+    "crates/lune-std-net",
+    "crates/lune-std-process",
+    "crates/lune-std-regex",
+    "crates/lune-std-roblox",
+    "crates/lune-std-serde",
+    "crates/lune-std-stdio",
+    "crates/lune-std-task",
+    "crates/lune-utils",
 ]
 
 # Profile for building the release binary, with the following options set:
@@ -53,99 +33,31 @@ opt-level = "z"
 strip = true
 lto = true
 
-# All of the dependencies for Lune.
+# Lints for all crates in the workspace
 #
-# Dependencies are categorized as following:
-#
-# 1. General dependencies with no specific features set
-# 2. Large / core dependencies that have many different crates and / or features set
-# 3. Dependencies for specific features of Lune, eg. the CLI or massive Roblox builtin library
-#
-[dependencies]
-console = "0.15"
-directories = "5.0"
-futures-util = "0.3"
-once_cell = "1.17"
-thiserror = "1.0"
-async-trait = "0.1"
-dialoguer = "0.11"
-dunce = "1.0"
-lz4_flex = "0.11"
-path-clean = "1.0"
-pathdiff = "0.2"
-pin-project = "1.0"
-urlencoding = "2.1"
-bstr = "1.9"
-regex = "1.10"
-self_cell = "1.0"
+# 1. Error on all lints by default, then make cargo + clippy pedantic lints just warn
+# 2. Selectively allow some lints that are _too_ pedantic, such as:
+#    - Casts between number types
+#    - Module naming conventions
+#    - Imports and multiple dependency versions
+[workspace.lints.clippy]
+all = { level = "deny", priority = -3 }
+cargo = { level = "warn", priority = -2 }
+pedantic = { level = "warn", priority = -1 }
 
-### RUNTIME
+cast_lossless = { level = "allow", priority = 1 }
+cast_possible_truncation = { level = "allow", priority = 1 }
+cast_possible_wrap = { level = "allow", priority = 1 }
+cast_precision_loss = { level = "allow", priority = 1 }
+cast_sign_loss = { level = "allow", priority = 1 }
 
-blocking = "1.5"
-tracing = "0.1"
-tracing-subscriber = { version = "0.3", features = ["env-filter"] }
-tokio = { version = "1.24", features = ["full", "tracing"] }
-os_str_bytes = { version = "7.0", features = ["conversions"] }
+similar_names = { level = "allow", priority = 1 }
+unnecessary_wraps = { level = "allow", priority = 1 }
+unnested_or_patterns = { level = "allow", priority = 1 }
+unreadable_literal = { level = "allow", priority = 1 }
 
-mlua-luau-scheduler = { version = "0.0.2" }
-mlua = { version = "0.9.7", features = [
-    "luau",
-    "luau-jit",
-    "async",
-    "serialize",
-] }
-
-### SERDE
-
-async-compression = { version = "0.4", features = [
-    "tokio",
-    "brotli",
-    "deflate",
-    "gzip",
-    "zlib",
-] }
-serde = { version = "1.0", features = ["derive"] }
-serde_json = { version = "1.0", features = ["preserve_order"] }
-serde_yaml = "0.9"
-toml = { version = "0.8", features = ["preserve_order"] }
-
-### NET
-
-hyper = { version = "1.1", features = ["full"] }
-hyper-util = { version = "0.1", features = ["full"] }
-http = "1.0"
-http-body-util = { version = "0.1" }
-hyper-tungstenite = { version = "0.13" }
-
-reqwest = { version = "0.11", default-features = false, features = [
-    "rustls-tls",
-] }
-
-tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
-
-### DATETIME
-chrono = "=0.4.34" # NOTE: 0.4.35 does not compile with chrono_lc
-chrono_lc = "0.1"
-
-### CLI
-
-anyhow = { optional = true, version = "1.0" }
-env_logger = { optional = true, version = "0.11" }
-itertools = "0.12"
-clap = { optional = true, version = "4.1", features = ["derive"] }
-include_dir = { optional = true, version = "0.7", features = ["glob"] }
-rustyline = { optional = true, version = "14.0" }
-zip_next = { optional = true, version = "1.1" }
-
-### ROBLOX
-
-glam = { optional = true, version = "0.27" }
-rand = { optional = true, version = "0.8" }
-
-rbx_cookie = { optional = true, version = "0.1.4", default-features = false }
-
-rbx_binary = { optional = true, version = "0.7.3" }
-rbx_dom_weak = { optional = true, version = "2.6.0" }
-rbx_reflection = { optional = true, version = "4.4.0" }
-rbx_reflection_database = { optional = true, version = "0.2.9" }
-rbx_xml = { optional = true, version = "0.13.2" }
+multiple_crate_versions = { level = "allow", priority = 1 }
+module_inception = { level = "allow", priority = 1 }
+module_name_repetitions = { level = "allow", priority = 1 }
+needless_pass_by_value = { level = "allow", priority = 1 }
+wildcard_imports = { level = "allow", priority = 1 }
diff --git a/aftman.toml b/aftman.toml
index 38cb739..3b4fcc9 100644
--- a/aftman.toml
+++ b/aftman.toml
@@ -1,4 +1,4 @@
 [tools]
-luau-lsp = "JohnnyMorganz/luau-lsp@1.27.0"
-selene = "Kampfkarren/selene@0.26.1"
-stylua = "JohnnyMorganz/StyLua@0.19.1"
+luau-lsp = "JohnnyMorganz/luau-lsp@1.29.0"
+selene = "Kampfkarren/selene@0.27.1"
+stylua = "JohnnyMorganz/StyLua@0.20.0"
diff --git a/crates/lune-roblox/Cargo.toml b/crates/lune-roblox/Cargo.toml
new file mode 100644
index 0000000..88eb441
--- /dev/null
+++ b/crates/lune-roblox/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "lune-roblox"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+
+glam = "0.27"
+rand = "0.8"
+thiserror = "1.0"
+once_cell = "1.17"
+
+rbx_binary = "0.7.3"
+rbx_dom_weak = "2.6.0"
+rbx_reflection = "4.4.0"
+rbx_reflection_database = "0.2.9"
+rbx_xml = "0.13.2"
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
diff --git a/src/roblox/datatypes/attributes.rs b/crates/lune-roblox/src/datatypes/attributes.rs
similarity index 81%
rename from src/roblox/datatypes/attributes.rs
rename to crates/lune-roblox/src/datatypes/attributes.rs
index d827801..7d5bd26 100644
--- a/src/roblox/datatypes/attributes.rs
+++ b/crates/lune-roblox/src/datatypes/attributes.rs
@@ -4,6 +4,15 @@ use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
 
 use super::extension::DomValueExt;
 
+/**
+    Checks if the given name is a valid attribute name.
+
+    # Errors
+
+    - If the name starts with the prefix "RBX".
+    - If the name contains any characters other than alphanumeric characters and underscore.
+    - If the name is longer than 100 characters.
+*/
 pub fn ensure_valid_attribute_name(name: impl AsRef<str>) -> LuaResult<()> {
     let name = name.as_ref();
     if name.to_ascii_uppercase().starts_with("RBX") {
@@ -23,6 +32,13 @@ pub fn ensure_valid_attribute_name(name: impl AsRef<str>) -> LuaResult<()> {
     }
 }
 
+/**
+    Checks if the given value is a valid attribute value.
+
+    # Errors
+
+    - If the value is not a valid attribute type.
+*/
 pub fn ensure_valid_attribute_value(value: &DomValue) -> LuaResult<()> {
     let is_valid = matches!(
         value.ty(),
diff --git a/src/roblox/datatypes/conversion.rs b/crates/lune-roblox/src/datatypes/conversion.rs
similarity index 98%
rename from src/roblox/datatypes/conversion.rs
rename to crates/lune-roblox/src/datatypes/conversion.rs
index f9b0a8e..f38ecc5 100644
--- a/src/roblox/datatypes/conversion.rs
+++ b/crates/lune-roblox/src/datatypes/conversion.rs
@@ -2,7 +2,7 @@ use mlua::prelude::*;
 
 use rbx_dom_weak::types::{Variant as DomValue, VariantType as DomType};
 
-use crate::roblox::{datatypes::extension::DomValueExt, instance::Instance};
+use crate::{datatypes::extension::DomValueExt, instance::Instance};
 
 use super::*;
 
@@ -65,8 +65,10 @@ impl<'lua> DomValueToLua<'lua> for LuaValue<'lua> {
 
                 // NOTE: Some values are either optional or default and we should handle
                 // that properly here since the userdata conversion above will always fail
-                DomValue::OptionalCFrame(None) => Ok(LuaValue::Nil),
-                DomValue::PhysicalProperties(dom::PhysicalProperties::Default) => Ok(LuaValue::Nil),
+                DomValue::OptionalCFrame(None)
+                | DomValue::PhysicalProperties(dom::PhysicalProperties::Default) => {
+                    Ok(LuaValue::Nil)
+                }
 
                 _ => Err(e),
             },
diff --git a/src/roblox/datatypes/extension.rs b/crates/lune-roblox/src/datatypes/extension.rs
similarity index 97%
rename from src/roblox/datatypes/extension.rs
rename to crates/lune-roblox/src/datatypes/extension.rs
index 30df47c..6f83242 100644
--- a/src/roblox/datatypes/extension.rs
+++ b/crates/lune-roblox/src/datatypes/extension.rs
@@ -6,6 +6,7 @@ pub(crate) trait DomValueExt {
 
 impl DomValueExt for DomType {
     fn variant_name(&self) -> Option<&'static str> {
+        #[allow(clippy::enum_glob_use)]
         use DomType::*;
         Some(match self {
             Attributes => "Attributes",
diff --git a/src/roblox/datatypes/mod.rs b/crates/lune-roblox/src/datatypes/mod.rs
similarity index 82%
rename from src/roblox/datatypes/mod.rs
rename to crates/lune-roblox/src/datatypes/mod.rs
index 5432c50..4da4715 100644
--- a/src/roblox/datatypes/mod.rs
+++ b/crates/lune-roblox/src/datatypes/mod.rs
@@ -10,4 +10,4 @@ mod util;
 
 use result::*;
 
-pub use crate::roblox::shared::userdata::*;
+pub use crate::shared::userdata::*;
diff --git a/src/roblox/datatypes/result.rs b/crates/lune-roblox/src/datatypes/result.rs
similarity index 100%
rename from src/roblox/datatypes/result.rs
rename to crates/lune-roblox/src/datatypes/result.rs
diff --git a/src/roblox/datatypes/types/axes.rs b/crates/lune-roblox/src/datatypes/types/axes.rs
similarity index 95%
rename from src/roblox/datatypes/types/axes.rs
rename to crates/lune-roblox/src/datatypes/types/axes.rs
index 0db23f3..e9aa950 100644
--- a/src/roblox/datatypes/types/axes.rs
+++ b/crates/lune-roblox/src/datatypes/types/axes.rs
@@ -3,7 +3,9 @@ use core::fmt;
 use mlua::prelude::*;
 use rbx_dom_weak::types::Axes as DomAxes;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, EnumItem};
 
@@ -52,8 +54,7 @@ impl LuaExportsTable<'_> for Axes {
                         check(&e);
                     } else {
                         return Err(LuaError::RuntimeError(format!(
-                            "Expected argument #{} to be an EnumItem, got userdata",
-                            index
+                            "Expected argument #{index} to be an EnumItem, got userdata",
                         )));
                     }
                 } else {
diff --git a/src/roblox/datatypes/types/brick_color.rs b/crates/lune-roblox/src/datatypes/types/brick_color.rs
similarity index 99%
rename from src/roblox/datatypes/types/brick_color.rs
rename to crates/lune-roblox/src/datatypes/types/brick_color.rs
index 8fa6f86..297793b 100644
--- a/src/roblox/datatypes/types/brick_color.rs
+++ b/crates/lune-roblox/src/datatypes/types/brick_color.rs
@@ -4,14 +4,16 @@ use mlua::prelude::*;
 use rand::seq::SliceRandom;
 use rbx_dom_weak::types::BrickColor as DomBrickColor;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, Color3};
 
 /**
     An implementation of the [BrickColor](https://create.roblox.com/docs/reference/engine/datatypes/BrickColor) Roblox datatype.
 
-    This implements all documented properties, methods & constructors of the BrickColor class as of March 2023.
+    This implements all documented properties, methods & constructors of the `BrickColor` class as of March 2023.
 */
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub struct BrickColor {
diff --git a/src/roblox/datatypes/types/cframe.rs b/crates/lune-roblox/src/datatypes/types/cframe.rs
similarity index 92%
rename from src/roblox/datatypes/types/cframe.rs
rename to crates/lune-roblox/src/datatypes/types/cframe.rs
index 4d7cbdf..adb8d88 100644
--- a/src/roblox/datatypes/types/cframe.rs
+++ b/crates/lune-roblox/src/datatypes/types/cframe.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::items_after_statements)]
+
 use core::fmt;
 use std::ops;
 
@@ -5,7 +7,9 @@ use glam::{EulerRot, Mat3, Mat4, Quat, Vec3};
 use mlua::{prelude::*, Variadic};
 use rbx_dom_weak::types::{CFrame as DomCFrame, Matrix3 as DomMatrix3, Vector3 as DomVector3};
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, Vector3};
 
@@ -14,7 +18,7 @@ use super::{super::*, Vector3};
     Roblox datatype, backed by [`glam::Mat4`].
 
     This implements all documented properties, methods &
-    constructors of the CFrame class as of March 2023.
+    constructors of the `CFrame` class as of March 2023.
 */
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub struct CFrame(pub Mat4);
@@ -42,6 +46,7 @@ impl CFrame {
 impl LuaExportsTable<'_> for CFrame {
     const EXPORT_NAME: &'static str = "CFrame";
 
+    #[allow(clippy::too_many_lines)]
     fn create_exports_table(lua: &Lua) -> LuaResult<LuaTable> {
         let cframe_angles = |_, (rx, ry, rz): (f32, f32, f32)| {
             Ok(CFrame(Mat4::from_euler(EulerRot::XYZ, rx, ry, rz)))
@@ -68,8 +73,7 @@ impl LuaExportsTable<'_> for CFrame {
             Ok(CFrame(Mat4::from_cols(
                 rx.0.extend(0.0),
                 ry.0.extend(0.0),
-                rz.map(|r| r.0)
-                    .unwrap_or_else(|| rx.0.cross(ry.0).normalize())
+                rz.map_or_else(|| rx.0.cross(ry.0).normalize(), |r| r.0)
                     .extend(0.0),
                 pos.0.extend(1.0),
             )))
@@ -195,6 +199,7 @@ impl LuaUserData for CFrame {
         });
     }
 
+    #[allow(clippy::too_many_lines)]
     fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
         // Methods
         methods.add_method("Inverse", |_, this, ()| Ok(this.inverse()));
@@ -226,34 +231,49 @@ impl LuaUserData for CFrame {
         methods.add_method(
             "ToWorldSpace",
             |_, this, rhs: Variadic<LuaUserDataRef<CFrame>>| {
-                Ok(Variadic::from_iter(rhs.into_iter().map(|cf| *this * *cf)))
+                Ok(rhs
+                    .into_iter()
+                    .map(|cf| *this * *cf)
+                    .collect::<Variadic<_>>())
             },
         );
         methods.add_method(
             "ToObjectSpace",
             |_, this, rhs: Variadic<LuaUserDataRef<CFrame>>| {
                 let inverse = this.inverse();
-                Ok(Variadic::from_iter(rhs.into_iter().map(|cf| inverse * *cf)))
+                Ok(rhs
+                    .into_iter()
+                    .map(|cf| inverse * *cf)
+                    .collect::<Variadic<_>>())
             },
         );
         methods.add_method(
             "PointToWorldSpace",
             |_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
-                Ok(Variadic::from_iter(rhs.into_iter().map(|v3| *this * *v3)))
+                Ok(rhs
+                    .into_iter()
+                    .map(|v3| *this * *v3)
+                    .collect::<Variadic<_>>())
             },
         );
         methods.add_method(
             "PointToObjectSpace",
             |_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
                 let inverse = this.inverse();
-                Ok(Variadic::from_iter(rhs.into_iter().map(|v3| inverse * *v3)))
+                Ok(rhs
+                    .into_iter()
+                    .map(|v3| inverse * *v3)
+                    .collect::<Variadic<_>>())
             },
         );
         methods.add_method(
             "VectorToWorldSpace",
             |_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
                 let result = *this - Vector3(this.position());
-                Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3)))
+                Ok(rhs
+                    .into_iter()
+                    .map(|v3| result * *v3)
+                    .collect::<Variadic<_>>())
             },
         );
         methods.add_method(
@@ -261,8 +281,10 @@ impl LuaUserData for CFrame {
             |_, this, rhs: Variadic<LuaUserDataRef<Vector3>>| {
                 let inverse = this.inverse();
                 let result = inverse - Vector3(inverse.position());
-
-                Ok(Variadic::from_iter(rhs.into_iter().map(|v3| result * *v3)))
+                Ok(rhs
+                    .into_iter()
+                    .map(|v3| result * *v3)
+                    .collect::<Variadic<_>>())
             },
         );
         #[rustfmt::skip]
@@ -445,7 +467,7 @@ mod cframe_test {
             Vec3::new(1.0, 2.0, 3.0).extend(1.0),
         ));
 
-        assert_eq!(CFrame::from(dom_cframe), cframe)
+        assert_eq!(CFrame::from(dom_cframe), cframe);
     }
 
     #[test]
@@ -466,6 +488,6 @@ mod cframe_test {
             ),
         );
 
-        assert_eq!(DomCFrame::from(cframe), dom_cframe)
+        assert_eq!(DomCFrame::from(cframe), dom_cframe);
     }
 }
diff --git a/src/roblox/datatypes/types/color3.rs b/crates/lune-roblox/src/datatypes/types/color3.rs
similarity index 97%
rename from src/roblox/datatypes/types/color3.rs
rename to crates/lune-roblox/src/datatypes/types/color3.rs
index 33eba6d..8523732 100644
--- a/src/roblox/datatypes/types/color3.rs
+++ b/crates/lune-roblox/src/datatypes/types/color3.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::many_single_char_names)]
+
 use core::fmt;
 use std::ops;
 
@@ -5,7 +7,9 @@ use glam::Vec3;
 use mlua::prelude::*;
 use rbx_dom_weak::types::{Color3 as DomColor3, Color3uint8 as DomColor3uint8};
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::super::*;
 
@@ -85,8 +89,7 @@ impl LuaExportsTable<'_> for Color3 {
                     b: (b as f32) / 255f32,
                 }),
                 _ => Err(LuaError::RuntimeError(format!(
-                    "Hex color string '{}' contains invalid character",
-                    trimmed
+                    "Hex color string '{trimmed}' contains invalid character",
                 ))),
             }
         };
@@ -151,6 +154,7 @@ impl LuaUserData for Color3 {
             let max = r.max(g).max(b);
             let diff = max - min;
 
+            #[allow(clippy::float_cmp)]
             let hue = (match max {
                 max if max == min => 0.0,
                 max if max == r => (g - b) / diff + (if g < b { 6.0 } else { 0.0 }),
diff --git a/src/roblox/datatypes/types/color_sequence.rs b/crates/lune-roblox/src/datatypes/types/color_sequence.rs
similarity index 93%
rename from src/roblox/datatypes/types/color_sequence.rs
rename to crates/lune-roblox/src/datatypes/types/color_sequence.rs
index a8eb8ab..f2cc1b4 100644
--- a/src/roblox/datatypes/types/color_sequence.rs
+++ b/crates/lune-roblox/src/datatypes/types/color_sequence.rs
@@ -5,14 +5,16 @@ use rbx_dom_weak::types::{
     ColorSequence as DomColorSequence, ColorSequenceKeypoint as DomColorSequenceKeypoint,
 };
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, Color3, ColorSequenceKeypoint};
 
 /**
     An implementation of the [ColorSequence](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequence) Roblox datatype.
 
-    This implements all documented properties, methods & constructors of the ColorSequence class as of March 2023.
+    This implements all documented properties, methods & constructors of the `ColorSequence` class as of March 2023.
 */
 #[derive(Debug, Clone, PartialEq)]
 pub struct ColorSequence {
@@ -87,9 +89,9 @@ impl fmt::Display for ColorSequence {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         for (index, keypoint) in self.keypoints.iter().enumerate() {
             if index < self.keypoints.len() - 1 {
-                write!(f, "{}, ", keypoint)?;
+                write!(f, "{keypoint}, ")?;
             } else {
-                write!(f, "{}", keypoint)?;
+                write!(f, "{keypoint}")?;
             }
         }
         Ok(())
@@ -102,7 +104,7 @@ impl From<DomColorSequence> for ColorSequence {
             keypoints: v
                 .keypoints
                 .iter()
-                .cloned()
+                .copied()
                 .map(ColorSequenceKeypoint::from)
                 .collect(),
         }
@@ -115,7 +117,7 @@ impl From<ColorSequence> for DomColorSequence {
             keypoints: v
                 .keypoints
                 .iter()
-                .cloned()
+                .copied()
                 .map(DomColorSequenceKeypoint::from)
                 .collect(),
         }
diff --git a/src/roblox/datatypes/types/color_sequence_keypoint.rs b/crates/lune-roblox/src/datatypes/types/color_sequence_keypoint.rs
similarity index 94%
rename from src/roblox/datatypes/types/color_sequence_keypoint.rs
rename to crates/lune-roblox/src/datatypes/types/color_sequence_keypoint.rs
index a90b6e7..c52b30e 100644
--- a/src/roblox/datatypes/types/color_sequence_keypoint.rs
+++ b/crates/lune-roblox/src/datatypes/types/color_sequence_keypoint.rs
@@ -3,14 +3,16 @@ use core::fmt;
 use mlua::prelude::*;
 use rbx_dom_weak::types::ColorSequenceKeypoint as DomColorSequenceKeypoint;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, Color3};
 
 /**
     An implementation of the [ColorSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/ColorSequenceKeypoint) Roblox datatype.
 
-    This implements all documented properties, methods & constructors of the ColorSequenceKeypoint class as of March 2023.
+    This implements all documented properties, methods & constructors of the `ColorSequenceKeypoint` class as of March 2023.
 */
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub struct ColorSequenceKeypoint {
diff --git a/src/roblox/datatypes/types/enum.rs b/crates/lune-roblox/src/datatypes/types/enum.rs
similarity index 100%
rename from src/roblox/datatypes/types/enum.rs
rename to crates/lune-roblox/src/datatypes/types/enum.rs
diff --git a/src/roblox/datatypes/types/enum_item.rs b/crates/lune-roblox/src/datatypes/types/enum_item.rs
similarity index 98%
rename from src/roblox/datatypes/types/enum_item.rs
rename to crates/lune-roblox/src/datatypes/types/enum_item.rs
index 8a3633c..7089ccc 100644
--- a/src/roblox/datatypes/types/enum_item.rs
+++ b/crates/lune-roblox/src/datatypes/types/enum_item.rs
@@ -8,7 +8,7 @@ use super::{super::*, Enum};
 /**
     An implementation of the [EnumItem](https://create.roblox.com/docs/reference/engine/datatypes/EnumItem) Roblox datatype.
 
-    This implements all documented properties, methods & constructors of the EnumItem class as of March 2023.
+    This implements all documented properties, methods & constructors of the `EnumItem` class as of March 2023.
 */
 #[derive(Debug, Clone)]
 pub struct EnumItem {
diff --git a/src/roblox/datatypes/types/enums.rs b/crates/lune-roblox/src/datatypes/types/enums.rs
similarity index 94%
rename from src/roblox/datatypes/types/enums.rs
rename to crates/lune-roblox/src/datatypes/types/enums.rs
index 5ce78d7..1a93597 100644
--- a/src/roblox/datatypes/types/enums.rs
+++ b/crates/lune-roblox/src/datatypes/types/enums.rs
@@ -24,8 +24,7 @@ impl LuaUserData for Enums {
             |_, _, name: String| match Enum::from_name(&name) {
                 Some(e) => Ok(e),
                 None => Err(LuaError::RuntimeError(format!(
-                    "The enum '{}' does not exist",
-                    name
+                    "The enum '{name}' does not exist",
                 ))),
             },
         );
diff --git a/src/roblox/datatypes/types/faces.rs b/crates/lune-roblox/src/datatypes/types/faces.rs
similarity index 95%
rename from src/roblox/datatypes/types/faces.rs
rename to crates/lune-roblox/src/datatypes/types/faces.rs
index b23ff72..61a6cd9 100644
--- a/src/roblox/datatypes/types/faces.rs
+++ b/crates/lune-roblox/src/datatypes/types/faces.rs
@@ -1,9 +1,13 @@
+#![allow(clippy::struct_excessive_bools)]
+
 use core::fmt;
 
 use mlua::prelude::*;
 use rbx_dom_weak::types::Faces as DomFaces;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, EnumItem};
 
@@ -54,8 +58,7 @@ impl LuaExportsTable<'_> for Faces {
                         check(&e);
                     } else {
                         return Err(LuaError::RuntimeError(format!(
-                            "Expected argument #{} to be an EnumItem, got userdata",
-                            index
+                            "Expected argument #{index} to be an EnumItem, got userdata",
                         )));
                     }
                 } else {
diff --git a/src/roblox/datatypes/types/font.rs b/crates/lune-roblox/src/datatypes/types/font.rs
similarity index 96%
rename from src/roblox/datatypes/types/font.rs
rename to crates/lune-roblox/src/datatypes/types/font.rs
index d3235d8..8848fea 100644
--- a/src/roblox/datatypes/types/font.rs
+++ b/crates/lune-roblox/src/datatypes/types/font.rs
@@ -6,7 +6,9 @@ use rbx_dom_weak::types::{
     Font as DomFont, FontStyle as DomFontStyle, FontWeight as DomFontWeight,
 };
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, EnumItem};
 
@@ -62,7 +64,7 @@ impl LuaExportsTable<'_> for Font {
         let font_from_name =
             |_, (file, weight, style): (String, Option<FontWeight>, Option<FontStyle>)| {
                 Ok(Font {
-                    family: format!("rbxasset://fonts/families/{}.json", file),
+                    family: format!("rbxasset://fonts/families/{file}.json"),
                     weight: weight.unwrap_or_default(),
                     style: style.unwrap_or_default(),
                     cached_id: None,
@@ -72,7 +74,7 @@ impl LuaExportsTable<'_> for Font {
         let font_from_id =
             |_, (id, weight, style): (i32, Option<FontWeight>, Option<FontStyle>)| {
                 Ok(Font {
-                    family: format!("rbxassetid://{}", id),
+                    family: format!("rbxassetid://{id}"),
                     weight: weight.unwrap_or_default(),
                     style: style.unwrap_or_default(),
                     cached_id: None,
@@ -206,7 +208,7 @@ pub(crate) enum FontWeight {
 }
 
 impl FontWeight {
-    pub(crate) fn as_u16(&self) -> u16 {
+    pub(crate) fn as_u16(self) -> u16 {
         match self {
             Self::Thin => 100,
             Self::ExtraLight => 200,
@@ -288,12 +290,11 @@ impl<'lua> FromLua<'lua> for FontWeight {
             if value.parent.desc.name == "FontWeight" {
                 if let Ok(value) = FontWeight::from_str(&value.name) {
                     return Ok(value);
-                } else {
-                    message = Some(format!(
-                        "Found unknown Enum.FontWeight value '{}'",
-                        value.name
-                    ));
                 }
+                message = Some(format!(
+                    "Found unknown Enum.FontWeight value '{}'",
+                    value.name
+                ));
             } else {
                 message = Some(format!(
                     "Expected Enum.FontWeight, got Enum.{}",
@@ -316,7 +317,7 @@ impl<'lua> IntoLua<'lua> for FontWeight {
             None => Err(LuaError::ToLuaConversionError {
                 from: "FontWeight",
                 to: "EnumItem",
-                message: Some(format!("Found unknown Enum.FontWeight value '{}'", self)),
+                message: Some(format!("Found unknown Enum.FontWeight value '{self}'")),
             }),
         }
     }
@@ -329,7 +330,7 @@ pub(crate) enum FontStyle {
 }
 
 impl FontStyle {
-    pub(crate) fn as_u8(&self) -> u8 {
+    pub(crate) fn as_u8(self) -> u8 {
         match self {
             Self::Normal => 0,
             Self::Italic => 1,
@@ -383,12 +384,11 @@ impl<'lua> FromLua<'lua> for FontStyle {
             if value.parent.desc.name == "FontStyle" {
                 if let Ok(value) = FontStyle::from_str(&value.name) {
                     return Ok(value);
-                } else {
-                    message = Some(format!(
-                        "Found unknown Enum.FontStyle value '{}'",
-                        value.name
-                    ));
                 }
+                message = Some(format!(
+                    "Found unknown Enum.FontStyle value '{}'",
+                    value.name
+                ));
             } else {
                 message = Some(format!(
                     "Expected Enum.FontStyle, got Enum.{}",
@@ -411,7 +411,7 @@ impl<'lua> IntoLua<'lua> for FontStyle {
             None => Err(LuaError::ToLuaConversionError {
                 from: "FontStyle",
                 to: "EnumItem",
-                message: Some(format!("Found unknown Enum.FontStyle value '{}'", self)),
+                message: Some(format!("Found unknown Enum.FontStyle value '{self}'")),
             }),
         }
     }
diff --git a/src/roblox/datatypes/types/mod.rs b/crates/lune-roblox/src/datatypes/types/mod.rs
similarity index 100%
rename from src/roblox/datatypes/types/mod.rs
rename to crates/lune-roblox/src/datatypes/types/mod.rs
diff --git a/src/roblox/datatypes/types/number_range.rs b/crates/lune-roblox/src/datatypes/types/number_range.rs
similarity index 94%
rename from src/roblox/datatypes/types/number_range.rs
rename to crates/lune-roblox/src/datatypes/types/number_range.rs
index b3b5ac9..ad839d6 100644
--- a/src/roblox/datatypes/types/number_range.rs
+++ b/crates/lune-roblox/src/datatypes/types/number_range.rs
@@ -3,14 +3,16 @@ use core::fmt;
 use mlua::prelude::*;
 use rbx_dom_weak::types::NumberRange as DomNumberRange;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::super::*;
 
 /**
     An implementation of the [NumberRange](https://create.roblox.com/docs/reference/engine/datatypes/NumberRange) Roblox datatype.
 
-    This implements all documented properties, methods & constructors of the NumberRange class as of March 2023.
+    This implements all documented properties, methods & constructors of the `NumberRange` class as of March 2023.
 */
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub struct NumberRange {
diff --git a/src/roblox/datatypes/types/number_sequence.rs b/crates/lune-roblox/src/datatypes/types/number_sequence.rs
similarity index 93%
rename from src/roblox/datatypes/types/number_sequence.rs
rename to crates/lune-roblox/src/datatypes/types/number_sequence.rs
index f6b80bb..12d7374 100644
--- a/src/roblox/datatypes/types/number_sequence.rs
+++ b/crates/lune-roblox/src/datatypes/types/number_sequence.rs
@@ -5,14 +5,16 @@ use rbx_dom_weak::types::{
     NumberSequence as DomNumberSequence, NumberSequenceKeypoint as DomNumberSequenceKeypoint,
 };
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, NumberSequenceKeypoint};
 
 /**
     An implementation of the [NumberSequence](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequence) Roblox datatype.
 
-    This implements all documented properties, methods & constructors of the NumberSequence class as of March 2023.
+    This implements all documented properties, methods & constructors of the `NumberSequence` class as of March 2023.
 */
 #[derive(Debug, Clone, PartialEq)]
 pub struct NumberSequence {
@@ -91,9 +93,9 @@ impl fmt::Display for NumberSequence {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         for (index, keypoint) in self.keypoints.iter().enumerate() {
             if index < self.keypoints.len() - 1 {
-                write!(f, "{}, ", keypoint)?;
+                write!(f, "{keypoint}, ")?;
             } else {
-                write!(f, "{}", keypoint)?;
+                write!(f, "{keypoint}")?;
             }
         }
         Ok(())
@@ -106,7 +108,7 @@ impl From<DomNumberSequence> for NumberSequence {
             keypoints: v
                 .keypoints
                 .iter()
-                .cloned()
+                .copied()
                 .map(NumberSequenceKeypoint::from)
                 .collect(),
         }
@@ -119,7 +121,7 @@ impl From<NumberSequence> for DomNumberSequence {
             keypoints: v
                 .keypoints
                 .iter()
-                .cloned()
+                .copied()
                 .map(DomNumberSequenceKeypoint::from)
                 .collect(),
         }
diff --git a/src/roblox/datatypes/types/number_sequence_keypoint.rs b/crates/lune-roblox/src/datatypes/types/number_sequence_keypoint.rs
similarity index 94%
rename from src/roblox/datatypes/types/number_sequence_keypoint.rs
rename to crates/lune-roblox/src/datatypes/types/number_sequence_keypoint.rs
index be9aa1c..9ec43e5 100644
--- a/src/roblox/datatypes/types/number_sequence_keypoint.rs
+++ b/crates/lune-roblox/src/datatypes/types/number_sequence_keypoint.rs
@@ -3,14 +3,16 @@ use core::fmt;
 use mlua::prelude::*;
 use rbx_dom_weak::types::NumberSequenceKeypoint as DomNumberSequenceKeypoint;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::super::*;
 
 /**
     An implementation of the [NumberSequenceKeypoint](https://create.roblox.com/docs/reference/engine/datatypes/NumberSequenceKeypoint) Roblox datatype.
 
-    This implements all documented properties, methods & constructors of the NumberSequenceKeypoint class as of March 2023.
+    This implements all documented properties, methods & constructors of the `NumberSequenceKeypoint` class as of March 2023.
 */
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub struct NumberSequenceKeypoint {
diff --git a/src/roblox/datatypes/types/physical_properties.rs b/crates/lune-roblox/src/datatypes/types/physical_properties.rs
similarity index 98%
rename from src/roblox/datatypes/types/physical_properties.rs
rename to crates/lune-roblox/src/datatypes/types/physical_properties.rs
index 198c9ce..fd558eb 100644
--- a/src/roblox/datatypes/types/physical_properties.rs
+++ b/crates/lune-roblox/src/datatypes/types/physical_properties.rs
@@ -3,14 +3,16 @@ use core::fmt;
 use mlua::prelude::*;
 use rbx_dom_weak::types::CustomPhysicalProperties as DomCustomPhysicalProperties;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, EnumItem};
 
 /**
     An implementation of the [PhysicalProperties](https://create.roblox.com/docs/reference/engine/datatypes/PhysicalProperties) Roblox datatype.
 
-    This implements all documented properties, methods & constructors of the PhysicalProperties class as of March 2023.
+    This implements all documented properties, methods & constructors of the `PhysicalProperties` class as of March 2023.
 */
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub struct PhysicalProperties {
diff --git a/src/roblox/datatypes/types/ray.rs b/crates/lune-roblox/src/datatypes/types/ray.rs
similarity index 97%
rename from src/roblox/datatypes/types/ray.rs
rename to crates/lune-roblox/src/datatypes/types/ray.rs
index 68aba6e..23a44d7 100644
--- a/src/roblox/datatypes/types/ray.rs
+++ b/crates/lune-roblox/src/datatypes/types/ray.rs
@@ -4,7 +4,9 @@ use glam::Vec3;
 use mlua::prelude::*;
 use rbx_dom_weak::types::Ray as DomRay;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, Vector3};
 
diff --git a/src/roblox/datatypes/types/rect.rs b/crates/lune-roblox/src/datatypes/types/rect.rs
similarity index 98%
rename from src/roblox/datatypes/types/rect.rs
rename to crates/lune-roblox/src/datatypes/types/rect.rs
index b305184..0fa805c 100644
--- a/src/roblox/datatypes/types/rect.rs
+++ b/crates/lune-roblox/src/datatypes/types/rect.rs
@@ -5,7 +5,9 @@ use glam::Vec2;
 use mlua::prelude::*;
 use rbx_dom_weak::types::Rect as DomRect;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, Vector2};
 
diff --git a/src/roblox/datatypes/types/region3.rs b/crates/lune-roblox/src/datatypes/types/region3.rs
similarity index 97%
rename from src/roblox/datatypes/types/region3.rs
rename to crates/lune-roblox/src/datatypes/types/region3.rs
index 9c9bdf0..cb44df7 100644
--- a/src/roblox/datatypes/types/region3.rs
+++ b/crates/lune-roblox/src/datatypes/types/region3.rs
@@ -4,7 +4,9 @@ use glam::{Mat4, Vec3};
 use mlua::prelude::*;
 use rbx_dom_weak::types::Region3 as DomRegion3;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, CFrame, Vector3};
 
diff --git a/src/roblox/datatypes/types/region3int16.rs b/crates/lune-roblox/src/datatypes/types/region3int16.rs
similarity index 96%
rename from src/roblox/datatypes/types/region3int16.rs
rename to crates/lune-roblox/src/datatypes/types/region3int16.rs
index d64ffde..61075b0 100644
--- a/src/roblox/datatypes/types/region3int16.rs
+++ b/crates/lune-roblox/src/datatypes/types/region3int16.rs
@@ -4,7 +4,9 @@ use glam::IVec3;
 use mlua::prelude::*;
 use rbx_dom_weak::types::Region3int16 as DomRegion3int16;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, Vector3int16};
 
diff --git a/src/roblox/datatypes/types/udim.rs b/crates/lune-roblox/src/datatypes/types/udim.rs
similarity index 96%
rename from src/roblox/datatypes/types/udim.rs
rename to crates/lune-roblox/src/datatypes/types/udim.rs
index b390c0c..ac728e8 100644
--- a/src/roblox/datatypes/types/udim.rs
+++ b/crates/lune-roblox/src/datatypes/types/udim.rs
@@ -4,14 +4,16 @@ use std::ops;
 use mlua::prelude::*;
 use rbx_dom_weak::types::UDim as DomUDim;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::super::*;
 
 /**
     An implementation of the [UDim](https://create.roblox.com/docs/reference/engine/datatypes/UDim) Roblox datatype.
 
-    This implements all documented properties, methods & constructors of the UDim class as of March 2023.
+    This implements all documented properties, methods & constructors of the `UDim` class as of March 2023.
 */
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub struct UDim {
diff --git a/src/roblox/datatypes/types/udim2.rs b/crates/lune-roblox/src/datatypes/types/udim2.rs
similarity index 97%
rename from src/roblox/datatypes/types/udim2.rs
rename to crates/lune-roblox/src/datatypes/types/udim2.rs
index 4a41f98..df78be4 100644
--- a/src/roblox/datatypes/types/udim2.rs
+++ b/crates/lune-roblox/src/datatypes/types/udim2.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::items_after_statements)]
+
 use core::fmt;
 use std::ops;
 
@@ -5,14 +7,16 @@ use glam::Vec2;
 use mlua::prelude::*;
 use rbx_dom_weak::types::UDim2 as DomUDim2;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::{super::*, UDim};
 
 /**
     An implementation of the [UDim2](https://create.roblox.com/docs/reference/engine/datatypes/UDim2) Roblox datatype.
 
-    This implements all documented properties, methods & constructors of the UDim2 class as of March 2023.
+    This implements all documented properties, methods & constructors of the `UDim2` class as of March 2023.
 */
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub struct UDim2 {
diff --git a/src/roblox/datatypes/types/vector2.rs b/crates/lune-roblox/src/datatypes/types/vector2.rs
similarity index 98%
rename from src/roblox/datatypes/types/vector2.rs
rename to crates/lune-roblox/src/datatypes/types/vector2.rs
index 0eaad3d..e0c352e 100644
--- a/src/roblox/datatypes/types/vector2.rs
+++ b/crates/lune-roblox/src/datatypes/types/vector2.rs
@@ -5,7 +5,9 @@ use glam::{Vec2, Vec3};
 use mlua::prelude::*;
 use rbx_dom_weak::types::Vector2 as DomVector2;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::super::*;
 
diff --git a/src/roblox/datatypes/types/vector2int16.rs b/crates/lune-roblox/src/datatypes/types/vector2int16.rs
similarity index 98%
rename from src/roblox/datatypes/types/vector2int16.rs
rename to crates/lune-roblox/src/datatypes/types/vector2int16.rs
index 1193428..31931a0 100644
--- a/src/roblox/datatypes/types/vector2int16.rs
+++ b/crates/lune-roblox/src/datatypes/types/vector2int16.rs
@@ -5,7 +5,9 @@ use glam::IVec2;
 use mlua::prelude::*;
 use rbx_dom_weak::types::Vector2int16 as DomVector2int16;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::super::*;
 
diff --git a/src/roblox/datatypes/types/vector3.rs b/crates/lune-roblox/src/datatypes/types/vector3.rs
similarity index 95%
rename from src/roblox/datatypes/types/vector3.rs
rename to crates/lune-roblox/src/datatypes/types/vector3.rs
index ef0307e..32387d3 100644
--- a/src/roblox/datatypes/types/vector3.rs
+++ b/crates/lune-roblox/src/datatypes/types/vector3.rs
@@ -5,10 +5,9 @@ use glam::Vec3;
 use mlua::prelude::*;
 use rbx_dom_weak::types::Vector3 as DomVector3;
 
-use crate::{
-    lune::util::TableBuilder,
-    roblox::{datatypes::util::round_float_decimal, exports::LuaExportsTable},
-};
+use lune_utils::TableBuilder;
+
+use crate::{datatypes::util::round_float_decimal, exports::LuaExportsTable};
 
 use super::{super::*, EnumItem};
 
@@ -37,8 +36,7 @@ impl LuaExportsTable<'_> for Vector3 {
                     "Z" => Vector3(Vec3::Z),
                     name => {
                         return Err(LuaError::RuntimeError(format!(
-                            "Axis '{}' is not known",
-                            name
+                            "Axis '{name}' is not known",
                         )))
                     }
                 })
@@ -61,8 +59,7 @@ impl LuaExportsTable<'_> for Vector3 {
                     "Back" => Vector3(Vec3::Z),
                     name => {
                         return Err(LuaError::RuntimeError(format!(
-                            "NormalId '{}' is not known",
-                            name
+                            "NormalId '{name}' is not known",
                         )))
                     }
                 })
diff --git a/src/roblox/datatypes/types/vector3int16.rs b/crates/lune-roblox/src/datatypes/types/vector3int16.rs
similarity index 98%
rename from src/roblox/datatypes/types/vector3int16.rs
rename to crates/lune-roblox/src/datatypes/types/vector3int16.rs
index d62e8ff..b8f4f31 100644
--- a/src/roblox/datatypes/types/vector3int16.rs
+++ b/crates/lune-roblox/src/datatypes/types/vector3int16.rs
@@ -5,7 +5,9 @@ use glam::IVec3;
 use mlua::prelude::*;
 use rbx_dom_weak::types::Vector3int16 as DomVector3int16;
 
-use crate::{lune::util::TableBuilder, roblox::exports::LuaExportsTable};
+use lune_utils::TableBuilder;
+
+use crate::exports::LuaExportsTable;
 
 use super::super::*;
 
diff --git a/src/roblox/datatypes/util.rs b/crates/lune-roblox/src/datatypes/util.rs
similarity index 100%
rename from src/roblox/datatypes/util.rs
rename to crates/lune-roblox/src/datatypes/util.rs
diff --git a/src/roblox/document/error.rs b/crates/lune-roblox/src/document/error.rs
similarity index 100%
rename from src/roblox/document/error.rs
rename to crates/lune-roblox/src/document/error.rs
diff --git a/src/roblox/document/format.rs b/crates/lune-roblox/src/document/format.rs
similarity index 100%
rename from src/roblox/document/format.rs
rename to crates/lune-roblox/src/document/format.rs
diff --git a/src/roblox/document/kind.rs b/crates/lune-roblox/src/document/kind.rs
similarity index 98%
rename from src/roblox/document/kind.rs
rename to crates/lune-roblox/src/document/kind.rs
index 0339578..eee19ba 100644
--- a/src/roblox/document/kind.rs
+++ b/crates/lune-roblox/src/document/kind.rs
@@ -2,7 +2,7 @@ use std::path::Path;
 
 use rbx_dom_weak::WeakDom;
 
-use crate::roblox::shared::instance::class_is_a_service;
+use crate::shared::instance::class_is_a_service;
 
 /**
     A document kind specifier.
@@ -58,6 +58,7 @@ impl DocumentKind {
 
         Returns `None` if the given dom is empty and as such can not have its kind inferred.
     */
+    #[must_use]
     pub fn from_weak_dom(dom: &WeakDom) -> Option<Self> {
         let mut has_top_level_child = false;
         let mut has_top_level_service = false;
diff --git a/src/roblox/document/mod.rs b/crates/lune-roblox/src/document/mod.rs
similarity index 91%
rename from src/roblox/document/mod.rs
rename to crates/lune-roblox/src/document/mod.rs
index 4f5cf00..5ef3c64 100644
--- a/src/roblox/document/mod.rs
+++ b/crates/lune-roblox/src/document/mod.rs
@@ -15,7 +15,7 @@ pub use kind::*;
 
 use postprocessing::*;
 
-use crate::roblox::instance::{data_model, Instance};
+use crate::instance::{data_model, Instance};
 
 pub type DocumentResult<T> = Result<T, DocumentError>;
 
@@ -78,6 +78,7 @@ impl Document {
         | Model | Binary | `rbxm`    |
         | Model | Xml    | `rbxmx`   |
     */
+    #[must_use]
 	#[rustfmt::skip]
     pub fn canonical_extension(kind: DocumentKind, format: DocumentFormat) -> &'static str {
         match (kind, format) {
@@ -113,6 +114,10 @@ impl Document {
         Note that detection of model vs place file is heavily dependent on the structure
         of the file, and a model file with services in it will detect as a place file, so
         if possible using [`Document::from_bytes`] with an explicit kind should be preferred.
+
+        # Errors
+
+        Errors if the given bytes are not a valid roblox file.
     */
     pub fn from_bytes_auto(bytes: impl AsRef<[u8]>) -> DocumentResult<Self> {
         let (format, dom) = Self::from_bytes_inner(bytes)?;
@@ -125,6 +130,10 @@ impl Document {
 
         This will automatically handle and detect if the document
         should be decoded using a roblox binary or roblox xml format.
+
+        # Errors
+
+        Errors if the given bytes are not a valid roblox file or not of the given kind.
     */
     pub fn from_bytes(bytes: impl AsRef<[u8]>, kind: DocumentKind) -> DocumentResult<Self> {
         let (format, dom) = Self::from_bytes_inner(bytes)?;
@@ -138,6 +147,10 @@ impl Document {
         This will use the same format that the document was created
         with, meaning if the document is a binary document the output
         will be binary, and vice versa for xml and other future formats.
+
+        # Errors
+
+        Errors if the document can not be encoded.
     */
     pub fn to_bytes(&self) -> DocumentResult<Vec<u8>> {
         self.to_bytes_with_format(self.format)
@@ -146,6 +159,10 @@ impl Document {
     /**
         Encodes the document as a vector of bytes, to
         be written to a file or sent over the network.
+
+        # Errors
+
+        Errors if the document can not be encoded.
     */
     pub fn to_bytes_with_format(&self, format: DocumentFormat) -> DocumentResult<Vec<u8>> {
         let mut bytes = Vec::new();
@@ -172,6 +189,7 @@ impl Document {
     /**
         Gets the kind this document was created with.
     */
+    #[must_use]
     pub fn kind(&self) -> DocumentKind {
         self.kind
     }
@@ -179,6 +197,7 @@ impl Document {
     /**
         Gets the format this document was created with.
     */
+    #[must_use]
     pub fn format(&self) -> DocumentFormat {
         self.format
     }
@@ -186,14 +205,17 @@ impl Document {
     /**
         Gets the file extension for this document.
     */
+    #[must_use]
     pub fn extension(&self) -> &'static str {
         Self::canonical_extension(self.kind, self.format)
     }
 
     /**
-        Creates a DataModel instance out of this place document.
+        Creates a `DataModel` instance out of this place document.
 
-        Will error if the document is not a place.
+        # Errors
+
+        Errors if the document is not a place.
     */
     pub fn into_data_model_instance(mut self) -> DocumentResult<Instance> {
         if self.kind != DocumentKind::Place {
@@ -219,7 +241,9 @@ impl Document {
     /**
         Creates an array of instances out of this model document.
 
-        Will error if the document is not a model.
+        # Errors
+
+        Errors if the document is not a model.
     */
     pub fn into_instance_array(mut self) -> DocumentResult<Vec<Instance>> {
         if self.kind != DocumentKind::Model {
@@ -237,9 +261,11 @@ impl Document {
     }
 
     /**
-        Creates a place document out of a DataModel instance.
+        Creates a place document out of a `DataModel` instance.
 
-        Will error if the instance is not a DataModel.
+        # Errors
+
+        Errors if the instance is not a `DataModel`.
     */
     pub fn from_data_model_instance(i: Instance) -> DocumentResult<Self> {
         if i.get_class_name() != data_model::CLASS_NAME {
@@ -266,7 +292,9 @@ impl Document {
     /**
         Creates a model document out of an array of instances.
 
-        Will error if any of the instances is a DataModel.
+        # Errors
+
+        Errors if any of the instances is a `DataModel`.
     */
     pub fn from_instance_array(v: Vec<Instance>) -> DocumentResult<Self> {
         for i in &v {
diff --git a/src/roblox/document/postprocessing.rs b/crates/lune-roblox/src/document/postprocessing.rs
similarity index 96%
rename from src/roblox/document/postprocessing.rs
rename to crates/lune-roblox/src/document/postprocessing.rs
index afa5b3f..69481f5 100644
--- a/src/roblox/document/postprocessing.rs
+++ b/crates/lune-roblox/src/document/postprocessing.rs
@@ -3,7 +3,7 @@ use rbx_dom_weak::{
     Instance as DomInstance, WeakDom,
 };
 
-use crate::roblox::shared::instance::class_is_a;
+use crate::shared::instance::class_is_a;
 
 pub fn postprocess_dom_for_place(_dom: &mut WeakDom) {
     // Nothing here yet
diff --git a/src/roblox/exports.rs b/crates/lune-roblox/src/exports.rs
similarity index 100%
rename from src/roblox/exports.rs
rename to crates/lune-roblox/src/exports.rs
diff --git a/src/roblox/instance/base.rs b/crates/lune-roblox/src/instance/base.rs
similarity index 93%
rename from src/roblox/instance/base.rs
rename to crates/lune-roblox/src/instance/base.rs
index 594952e..cc35373 100644
--- a/src/roblox/instance/base.rs
+++ b/crates/lune-roblox/src/instance/base.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::items_after_statements)]
+
 use mlua::prelude::*;
 
 use rbx_dom_weak::{
@@ -5,7 +7,7 @@ use rbx_dom_weak::{
     Instance as DomInstance,
 };
 
-use crate::roblox::{
+use crate::{
     datatypes::{
         attributes::{ensure_valid_attribute_name, ensure_valid_attribute_value},
         conversion::{DomValueToLua, LuaToDomValue},
@@ -17,6 +19,7 @@ use crate::roblox::{
 
 use super::{data_model, registry::InstanceRegistry, Instance};
 
+#[allow(clippy::too_many_lines)]
 pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
     m.add_meta_method(LuaMetaMethod::ToString, |lua, this, ()| {
         ensure_not_destroyed(this)?;
@@ -142,7 +145,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(m: &mut M) {
         ensure_not_destroyed(this)?;
         let attributes = this.get_attributes();
         let tab = lua.create_table_with_capacity(0, attributes.len())?;
-        for (key, value) in attributes.into_iter() {
+        for (key, value) in attributes {
             tab.set(key, LuaValue::dom_value_to_lua(lua, &value)?)?;
         }
         Ok(tab)
@@ -227,8 +230,7 @@ fn instance_property_get<'lua>(
             if let DomValue::Enum(enum_value) = prop {
                 let enum_name = info.enum_name.ok_or_else(|| {
                     LuaError::RuntimeError(format!(
-                        "Failed to get property '{}' - encountered unknown enum",
-                        prop_name
+                        "Failed to get property '{prop_name}' - encountered unknown enum",
                     ))
                 })?;
                 EnumItem::from_enum_name_and_value(&enum_name, enum_value.to_u32())
@@ -246,8 +248,7 @@ fn instance_property_get<'lua>(
             EnumItem::from_enum_name_and_value(&enum_name, enum_value)
                 .ok_or_else(|| {
                     LuaError::RuntimeError(format!(
-                        "Failed to get property '{}' - Enum.{} does not contain numeric value {}",
-                        prop_name, enum_name, enum_value
+                        "Failed to get property '{prop_name}' - Enum.{enum_name} does not contain numeric value {enum_value}",
                     ))
                 })?
                 .into_lua(lua)
@@ -258,14 +259,12 @@ fn instance_property_get<'lua>(
                 Ok(LuaValue::Nil)
             } else {
                 Err(LuaError::RuntimeError(format!(
-                    "Failed to get property '{}' - missing default value",
-                    prop_name
+                    "Failed to get property '{prop_name}' - missing default value",
                 )))
             }
         } else {
             Err(LuaError::RuntimeError(format!(
-                "Failed to get property '{}' - malformed property info",
-                prop_name
+                "Failed to get property '{prop_name}' - malformed property info",
             )))
         }
     } else if let Some(inst) = this.find_child(|inst| inst.name == prop_name) {
@@ -276,8 +275,7 @@ fn instance_property_get<'lua>(
         Ok(LuaValue::Function(method))
     } else {
         Err(LuaError::RuntimeError(format!(
-            "{} is not a valid member of {}",
-            prop_name, this
+            "{prop_name} is not a valid member of {this}",
         )))
     }
 }
@@ -347,16 +345,14 @@ fn instance_property_set<'lua>(
             }
         } else {
             Err(LuaError::RuntimeError(format!(
-                "Failed to set property '{}' - malformed property info",
-                prop_name
+                "Failed to set property '{prop_name}' - malformed property info",
             )))
         }
     } else if let Some(setter) = InstanceRegistry::find_property_setter(lua, this, &prop_name) {
         setter.call((this.clone(), prop_value))
     } else {
         Err(LuaError::RuntimeError(format!(
-            "{} is not a valid member of {}",
-            prop_name, this
+            "{prop_name} is not a valid member of {this}",
         )))
     }
 }
diff --git a/src/roblox/instance/data_model.rs b/crates/lune-roblox/src/instance/data_model.rs
similarity index 89%
rename from src/roblox/instance/data_model.rs
rename to crates/lune-roblox/src/instance/data_model.rs
index 08d99be..cfbc9d5 100644
--- a/src/roblox/instance/data_model.rs
+++ b/crates/lune-roblox/src/instance/data_model.rs
@@ -1,6 +1,6 @@
 use mlua::prelude::*;
 
-use crate::roblox::shared::{
+use crate::shared::{
     classes::{
         add_class_restricted_getter, add_class_restricted_method,
         get_or_create_property_ref_instance,
@@ -33,7 +33,7 @@ fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult<Instance> {
 }
 
 /**
-    Gets or creates a service for this DataModel.
+    Gets or creates a service for this `DataModel`.
 
     ### See Also
     * [`GetService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#GetService)
@@ -42,8 +42,7 @@ fn data_model_get_workspace(_: &Lua, this: &Instance) -> LuaResult<Instance> {
 fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> LuaResult<Instance> {
     if matches!(class_is_a_service(&service_name), None | Some(false)) {
         Err(LuaError::RuntimeError(format!(
-            "'{}' is not a valid service name",
-            service_name
+            "'{service_name}' is not a valid service name",
         )))
     } else if let Some(service) = this.find_child(|child| child.class == service_name) {
         Ok(service)
@@ -55,7 +54,7 @@ fn data_model_get_service(_: &Lua, this: &Instance, service_name: String) -> Lua
 }
 
 /**
-    Gets a service for this DataModel, if it exists.
+    Gets a service for this `DataModel`, if it exists.
 
     ### See Also
     * [`FindService`](https://create.roblox.com/docs/reference/engine/classes/ServiceProvider#FindService)
@@ -68,8 +67,7 @@ fn data_model_find_service(
 ) -> LuaResult<Option<Instance>> {
     if matches!(class_is_a_service(&service_name), None | Some(false)) {
         Err(LuaError::RuntimeError(format!(
-            "'{}' is not a valid service name",
-            service_name
+            "'{service_name}' is not a valid service name",
         )))
     } else if let Some(service) = this.find_child(|child| child.class == service_name) {
         Ok(Some(service))
diff --git a/src/roblox/instance/mod.rs b/crates/lune-roblox/src/instance/mod.rs
similarity index 96%
rename from src/roblox/instance/mod.rs
rename to crates/lune-roblox/src/instance/mod.rs
index 6473b04..bc75f2f 100644
--- a/src/roblox/instance/mod.rs
+++ b/crates/lune-roblox/src/instance/mod.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::missing_panics_doc)]
+
 use std::{
     collections::{BTreeMap, VecDeque},
     fmt,
@@ -12,10 +14,11 @@ use rbx_dom_weak::{
     Instance as DomInstance, InstanceBuilder as DomInstanceBuilder, WeakDom,
 };
 
+use lune_utils::TableBuilder;
+
 use crate::{
-    lune::util::TableBuilder,
-    roblox::exports::LuaExportsTable,
-    roblox::shared::instance::{class_exists, class_is_a},
+    exports::LuaExportsTable,
+    shared::instance::{class_exists, class_is_a},
 };
 
 pub(crate) mod base;
@@ -54,9 +57,10 @@ impl Instance {
             .get_by_ref(dom_ref)
             .expect("Failed to find instance in document");
 
-        if instance.referent() == dom.root_ref() {
-            panic!("Instances can not be created from dom roots")
-        }
+        assert!(
+            !(instance.referent() == dom.root_ref()),
+            "Instances can not be created from dom roots"
+        );
 
         Self {
             dom_ref,
@@ -76,9 +80,10 @@ impl Instance {
         let dom = INTERNAL_DOM.lock().expect("Failed to lock document");
 
         if let Some(instance) = dom.get_by_ref(dom_ref) {
-            if instance.referent() == dom.root_ref() {
-                panic!("Instances can not be created from dom roots")
-            }
+            assert!(
+                !(instance.referent() == dom.root_ref()),
+                "Instances can not be created from dom roots"
+            );
 
             Some(Self {
                 dom_ref,
@@ -154,7 +159,7 @@ impl Instance {
 
         let cloned = dom.clone_multiple_into_external(referents, external_dom);
 
-        for referent in cloned.iter() {
+        for referent in &cloned {
             external_dom.transfer_within(*referent, external_dom.root_ref());
         }
 
@@ -171,7 +176,8 @@ impl Instance {
         * [`Clone`](https://create.roblox.com/docs/reference/engine/classes/Instance#Clone)
         on the Roblox Developer Hub
     */
-    pub fn clone_instance(&self) -> Instance {
+    #[must_use]
+    pub fn clone_instance(&self) -> Self {
         let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
         let new_ref = dom.clone_within(self.dom_ref);
         drop(dom); // Self::new needs mutex handle, drop it first
@@ -254,6 +260,7 @@ impl Instance {
         * [`ClassName`](https://create.roblox.com/docs/reference/engine/classes/Instance#ClassName)
         on the Roblox Developer Hub
     */
+    #[must_use]
     pub fn get_class_name(&self) -> &str {
         self.class_name.as_str()
     }
@@ -286,7 +293,7 @@ impl Instance {
 
         dom.get_by_ref_mut(self.dom_ref)
             .expect("Failed to find instance in document")
-            .name = name.into()
+            .name = name.into();
     }
 
     /**
@@ -326,9 +333,7 @@ impl Instance {
     pub fn set_parent(&self, parent: Option<Instance>) {
         let mut dom = INTERNAL_DOM.lock().expect("Failed to lock document");
 
-        let parent_ref = parent
-            .map(|parent| parent.dom_ref)
-            .unwrap_or_else(|| dom.root_ref());
+        let parent_ref = parent.map_or_else(|| dom.root_ref(), |parent| parent.dom_ref);
 
         dom.transfer_within(self.dom_ref, parent_ref);
     }
@@ -663,9 +668,8 @@ impl Instance {
             if predicate(ancestor) {
                 drop(dom); // Self::new needs mutex handle, drop it first
                 return Some(Self::new(ancestor_ref));
-            } else {
-                ancestor_ref = ancestor.parent();
             }
+            ancestor_ref = ancestor.parent();
         }
 
         None
@@ -699,9 +703,8 @@ impl Instance {
                 let queue_ref = queue_item.referent();
                 drop(dom); // Self::new needs mutex handle, drop it first
                 return Some(Self::new(queue_ref));
-            } else {
-                queue.extend(queue_item.children())
             }
+            queue.extend(queue_item.children());
         }
 
         None
@@ -717,8 +720,7 @@ impl LuaExportsTable<'_> for Instance {
                 Instance::new_orphaned(class_name).into_lua(lua)
             } else {
                 Err(LuaError::RuntimeError(format!(
-                    "Failed to create Instance - '{}' is not a valid class name",
-                    class_name
+                    "Failed to create Instance - '{class_name}' is not a valid class name",
                 )))
             }
         };
@@ -756,7 +758,7 @@ impl LuaUserData for Instance {
 
 impl Hash for Instance {
     fn hash<H: Hasher>(&self, state: &mut H) {
-        self.dom_ref.hash(state)
+        self.dom_ref.hash(state);
     }
 }
 
diff --git a/src/roblox/instance/registry.rs b/crates/lune-roblox/src/instance/registry.rs
similarity index 86%
rename from src/roblox/instance/registry.rs
rename to crates/lune-roblox/src/instance/registry.rs
index 1dfd6f0..11aa175 100644
--- a/src/roblox/instance/registry.rs
+++ b/crates/lune-roblox/src/instance/registry.rs
@@ -51,6 +51,13 @@ impl InstanceRegistry {
             .expect("Missing InstanceRegistry in app data")
     }
 
+    /**
+        Inserts a method into the instance registry.
+
+        # Errors
+
+        - If the method already exists in the registry.
+    */
     pub fn insert_method<'lua>(
         lua: &'lua Lua,
         class_name: &str,
@@ -80,6 +87,13 @@ impl InstanceRegistry {
         Ok(())
     }
 
+    /**
+        Inserts a property getter into the instance registry.
+
+        # Errors
+
+        - If the property already exists in the registry.
+    */
     pub fn insert_property_getter<'lua>(
         lua: &'lua Lua,
         class_name: &str,
@@ -109,6 +123,13 @@ impl InstanceRegistry {
         Ok(())
     }
 
+    /**
+        Inserts a property setter into the instance registry.
+
+        # Errors
+
+        - If the property already exists in the registry.
+    */
     pub fn insert_property_setter<'lua>(
         lua: &'lua Lua,
         class_name: &str,
@@ -138,6 +159,12 @@ impl InstanceRegistry {
         Ok(())
     }
 
+    /**
+        Finds a method in the instance registry.
+
+        Returns `None` if the method is not found.
+    */
+    #[must_use]
     pub fn find_method<'lua>(
         lua: &'lua Lua,
         instance: &Instance,
@@ -159,6 +186,12 @@ impl InstanceRegistry {
             })
     }
 
+    /**
+        Finds a property getter in the instance registry.
+
+        Returns `None` if the property getter is not found.
+    */
+    #[must_use]
     pub fn find_property_getter<'lua>(
         lua: &'lua Lua,
         instance: &Instance,
@@ -180,6 +213,12 @@ impl InstanceRegistry {
             })
     }
 
+    /**
+        Finds a property setter in the instance registry.
+
+        Returns `None` if the property setter is not found.
+    */
+    #[must_use]
     pub fn find_property_setter<'lua>(
         lua: &'lua Lua,
         instance: &Instance,
@@ -202,6 +241,16 @@ impl InstanceRegistry {
     }
 }
 
+/**
+    Gets the class name chain for a given class name.
+
+    The chain starts with the given class name and ends with the root class.
+
+    # Panics
+
+    Panics if the class name is not valid.
+*/
+#[must_use]
 pub fn class_name_chain(class_name: &str) -> Vec<&str> {
     let db = rbx_reflection_database::get();
 
diff --git a/src/roblox/instance/terrain.rs b/crates/lune-roblox/src/instance/terrain.rs
similarity index 99%
rename from src/roblox/instance/terrain.rs
rename to crates/lune-roblox/src/instance/terrain.rs
index fad8e3d..852f321 100644
--- a/src/roblox/instance/terrain.rs
+++ b/crates/lune-roblox/src/instance/terrain.rs
@@ -1,7 +1,7 @@
 use mlua::prelude::*;
 use rbx_dom_weak::types::{MaterialColors, TerrainMaterials, Variant};
 
-use crate::roblox::{
+use crate::{
     datatypes::types::{Color3, EnumItem},
     shared::classes::{add_class_restricted_method, add_class_restricted_method_mut},
 };
@@ -23,7 +23,7 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M)
         CLASS_NAME,
         "SetMaterialColor",
         terrain_set_material_color,
-    )
+    );
 }
 
 fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
diff --git a/src/roblox/instance/workspace.rs b/crates/lune-roblox/src/instance/workspace.rs
similarity index 90%
rename from src/roblox/instance/workspace.rs
rename to crates/lune-roblox/src/instance/workspace.rs
index 1af3f54..70f1b88 100644
--- a/src/roblox/instance/workspace.rs
+++ b/crates/lune-roblox/src/instance/workspace.rs
@@ -1,8 +1,6 @@
 use mlua::prelude::*;
 
-use crate::roblox::shared::classes::{
-    add_class_restricted_getter, get_or_create_property_ref_instance,
-};
+use crate::shared::classes::{add_class_restricted_getter, get_or_create_property_ref_instance};
 
 use super::Instance;
 
diff --git a/src/roblox/mod.rs b/crates/lune-roblox/src/lib.rs
similarity index 81%
rename from src/roblox/mod.rs
rename to crates/lune-roblox/src/lib.rs
index 6b0f938..878a2f2 100644
--- a/src/roblox/mod.rs
+++ b/crates/lune-roblox/src/lib.rs
@@ -1,6 +1,8 @@
+#![allow(clippy::cargo_common_metadata)]
+
 use mlua::prelude::*;
 
-use crate::lune::util::TableBuilder;
+use lune_utils::TableBuilder;
 
 pub mod datatypes;
 pub mod document;
@@ -46,6 +48,16 @@ fn create_all_exports(lua: &Lua) -> LuaResult<Vec<(&'static str, LuaValue)>> {
     ])
 }
 
+/**
+    Creates a table containing all the Roblox datatypes, classes, and singletons.
+
+    Note that this is not guaranteed to contain any value unless indexed directly,
+    it may be optimized to use lazy initialization in the future.
+
+    # Errors
+
+    Errors when out of memory or when a value cannot be created.
+*/
 pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
     // FUTURE: We can probably create these lazily as users
     // index the main exports (this return value) table and
diff --git a/src/roblox/reflection/class.rs b/crates/lune-roblox/src/reflection/class.rs
similarity index 91%
rename from src/roblox/reflection/class.rs
rename to crates/lune-roblox/src/reflection/class.rs
index 13ce84a..f5b2aac 100644
--- a/src/roblox/reflection/class.rs
+++ b/crates/lune-roblox/src/reflection/class.rs
@@ -7,7 +7,7 @@ use rbx_dom_weak::types::Variant as DomVariant;
 use rbx_reflection::{ClassDescriptor, DataType};
 
 use super::{property::DatabaseProperty, utils::*};
-use crate::roblox::datatypes::{
+use crate::datatypes::{
     conversion::DomValueToLua, types::EnumItem, userdata_impl_eq, userdata_impl_to_string,
 };
 
@@ -28,6 +28,7 @@ impl DatabaseClass {
     /**
         Get the name of this class.
     */
+    #[must_use]
     pub fn get_name(&self) -> String {
         self.0.name.to_string()
     }
@@ -37,6 +38,7 @@ impl DatabaseClass {
 
         May be `None` if no parent class exists.
     */
+    #[must_use]
     pub fn get_superclass(&self) -> Option<String> {
         let sup = self.0.superclass.as_ref()?;
         Some(sup.to_string())
@@ -45,6 +47,7 @@ impl DatabaseClass {
     /**
         Get all known properties for this class.
     */
+    #[must_use]
     pub fn get_properties(&self) -> HashMap<String, DatabaseProperty> {
         self.0
             .properties
@@ -56,6 +59,7 @@ impl DatabaseClass {
     /**
         Get all default values for properties of this class.
     */
+    #[must_use]
     pub fn get_defaults(&self) -> HashMap<String, DomVariant> {
         self.0
             .default_properties
@@ -71,7 +75,12 @@ impl DatabaseClass {
         to players at runtime, and top-level class categories.
     */
     pub fn get_tags_str(&self) -> Vec<&'static str> {
-        self.0.tags.iter().map(class_tag_to_str).collect::<Vec<_>>()
+        self.0
+            .tags
+            .iter()
+            .copied()
+            .map(class_tag_to_str)
+            .collect::<Vec<_>>()
     }
 }
 
@@ -135,14 +144,12 @@ fn make_enum_value(inner: DbClass, name: impl AsRef<str>, value: u32) -> LuaResu
     let name = name.as_ref();
     let enum_name = find_enum_name(inner, name).ok_or_else(|| {
         LuaError::RuntimeError(format!(
-            "Failed to get default property '{}' - No enum descriptor was found",
-            name
+            "Failed to get default property '{name}' - No enum descriptor was found",
         ))
     })?;
     EnumItem::from_enum_name_and_value(&enum_name, value).ok_or_else(|| {
         LuaError::RuntimeError(format!(
-            "Failed to get default property '{}' - Enum.{} does not contain numeric value {}",
-            name, enum_name, value
+            "Failed to get default property '{name}' - Enum.{enum_name} does not contain numeric value {value}",
         ))
     })
 }
diff --git a/src/roblox/reflection/enums.rs b/crates/lune-roblox/src/reflection/enums.rs
similarity index 91%
rename from src/roblox/reflection/enums.rs
rename to crates/lune-roblox/src/reflection/enums.rs
index 2ed29a0..62f786c 100644
--- a/src/roblox/reflection/enums.rs
+++ b/crates/lune-roblox/src/reflection/enums.rs
@@ -4,7 +4,7 @@ use mlua::prelude::*;
 
 use rbx_reflection::EnumDescriptor;
 
-use crate::roblox::datatypes::{userdata_impl_eq, userdata_impl_to_string};
+use crate::datatypes::{userdata_impl_eq, userdata_impl_to_string};
 
 type DbEnum = &'static EnumDescriptor<'static>;
 
@@ -23,6 +23,7 @@ impl DatabaseEnum {
     /**
         Get the name of this enum.
     */
+    #[must_use]
     pub fn get_name(&self) -> String {
         self.0.name.to_string()
     }
@@ -31,8 +32,9 @@ impl DatabaseEnum {
         Get all known members of this enum.
 
         Note that this is a direct map of name -> enum values,
-        and does not actually use the EnumItem datatype itself.
+        and does not actually use the `EnumItem` datatype itself.
     */
+    #[must_use]
     pub fn get_items(&self) -> HashMap<String, u32> {
         self.0
             .items
diff --git a/src/roblox/reflection/mod.rs b/crates/lune-roblox/src/reflection/mod.rs
similarity index 89%
rename from src/roblox/reflection/mod.rs
rename to crates/lune-roblox/src/reflection/mod.rs
index 3b59200..9bb2203 100644
--- a/src/roblox/reflection/mod.rs
+++ b/crates/lune-roblox/src/reflection/mod.rs
@@ -4,7 +4,7 @@ use mlua::prelude::*;
 
 use rbx_reflection::ReflectionDatabase;
 
-use crate::roblox::datatypes::userdata_impl_eq;
+use crate::datatypes::userdata_impl_eq;
 
 mod class;
 mod enums;
@@ -30,6 +30,7 @@ impl Database {
     /**
         Creates a new database struct, referencing the bundled reflection database.
     */
+    #[must_use]
     pub fn new() -> Self {
         Self::default()
     }
@@ -40,6 +41,7 @@ impl Database {
         This will follow the format `x.y.z.w`, which most
         commonly looks something like `0.567.0.123456789`.
     */
+    #[must_use]
     pub fn get_version(&self) -> String {
         let [x, y, z, w] = self.0.version;
         format!("{x}.{y}.{z}.{w}")
@@ -48,15 +50,17 @@ impl Database {
     /**
         Retrieves a list of all currently known enum names.
     */
+    #[must_use]
     pub fn get_enum_names(&self) -> Vec<String> {
-        self.0.enums.keys().map(|e| e.to_string()).collect()
+        self.0.enums.keys().map(ToString::to_string).collect()
     }
 
     /**
         Retrieves a list of all currently known class names.
     */
+    #[must_use]
     pub fn get_class_names(&self) -> Vec<String> {
-        self.0.classes.keys().map(|e| e.to_string()).collect()
+        self.0.classes.keys().map(ToString::to_string).collect()
     }
 
     /**
@@ -108,14 +112,17 @@ impl Database {
 
 impl LuaUserData for Database {
     fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) {
-        fields.add_field_method_get("Version", |_, this| Ok(this.get_version()))
+        fields.add_field_method_get("Version", |_, this| Ok(this.get_version()));
     }
 
     fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
         methods.add_meta_method(LuaMetaMethod::Eq, userdata_impl_eq);
         methods.add_meta_method(LuaMetaMethod::ToString, userdata_impl_to_string);
-        methods.add_method("GetEnumNames", |_, this, _: ()| Ok(this.get_enum_names()));
-        methods.add_method("GetClassNames", |_, this, _: ()| Ok(this.get_class_names()));
+        methods.add_method("GetEnumNames", |_, this, (): ()| Ok(this.get_enum_names()));
+        methods.add_method(
+            "GetClassNames",
+            |_, this, (): ()| Ok(this.get_class_names()),
+        );
         methods.add_method("GetEnum", |_, this, name: String| Ok(this.get_enum(name)));
         methods.add_method("GetClass", |_, this, name: String| Ok(this.get_class(name)));
         methods.add_method("FindEnum", |_, this, name: String| Ok(this.find_enum(name)));
diff --git a/src/roblox/reflection/property.rs b/crates/lune-roblox/src/reflection/property.rs
similarity index 93%
rename from src/roblox/reflection/property.rs
rename to crates/lune-roblox/src/reflection/property.rs
index d674d3c..3e169b3 100644
--- a/src/roblox/reflection/property.rs
+++ b/crates/lune-roblox/src/reflection/property.rs
@@ -5,7 +5,7 @@ use mlua::prelude::*;
 use rbx_reflection::{ClassDescriptor, PropertyDescriptor};
 
 use super::utils::*;
-use crate::roblox::datatypes::{userdata_impl_eq, userdata_impl_to_string};
+use crate::datatypes::{userdata_impl_eq, userdata_impl_to_string};
 
 type DbClass = &'static ClassDescriptor<'static>;
 type DbProp = &'static PropertyDescriptor<'static>;
@@ -25,6 +25,7 @@ impl DatabaseProperty {
     /**
         Get the name of this property.
     */
+    #[must_use]
     pub fn get_name(&self) -> String {
         self.1.name.to_string()
     }
@@ -36,6 +37,7 @@ impl DatabaseProperty {
 
         For enums this will be a string formatted as `Enum.EnumName`.
     */
+    #[must_use]
     pub fn get_datatype_name(&self) -> String {
         data_type_to_str(self.1.data_type.clone())
     }
@@ -45,8 +47,9 @@ impl DatabaseProperty {
 
         All properties are writable and readable in Lune even if scriptability is not.
     */
+    #[must_use]
     pub fn get_scriptability_str(&self) -> &'static str {
-        scriptability_to_str(&self.1.scriptability)
+        scriptability_to_str(self.1.scriptability)
     }
 
     /**
@@ -59,6 +62,7 @@ impl DatabaseProperty {
         self.1
             .tags
             .iter()
+            .copied()
             .map(property_tag_to_str)
             .collect::<Vec<_>>()
     }
diff --git a/src/roblox/reflection/utils.rs b/crates/lune-roblox/src/reflection/utils.rs
similarity index 86%
rename from src/roblox/reflection/utils.rs
rename to crates/lune-roblox/src/reflection/utils.rs
index 84affd1..aa2042b 100644
--- a/src/roblox/reflection/utils.rs
+++ b/crates/lune-roblox/src/reflection/utils.rs
@@ -1,6 +1,6 @@
 use rbx_reflection::{ClassTag, DataType, PropertyTag, Scriptability};
 
-use crate::roblox::datatypes::extension::DomValueExt;
+use crate::datatypes::extension::DomValueExt;
 
 pub fn data_type_to_str(data_type: DataType) -> String {
     match data_type {
@@ -17,7 +17,7 @@ pub fn data_type_to_str(data_type: DataType) -> String {
      NOTE: Remember to add any new strings here to typedefs too!
 */
 
-pub fn scriptability_to_str(scriptability: &Scriptability) -> &'static str {
+pub fn scriptability_to_str(scriptability: Scriptability) -> &'static str {
     match scriptability {
         Scriptability::None => "None",
         Scriptability::Custom => "Custom",
@@ -28,7 +28,7 @@ pub fn scriptability_to_str(scriptability: &Scriptability) -> &'static str {
     }
 }
 
-pub fn property_tag_to_str(tag: &PropertyTag) -> &'static str {
+pub fn property_tag_to_str(tag: PropertyTag) -> &'static str {
     match tag {
         PropertyTag::Deprecated => "Deprecated",
         PropertyTag::Hidden => "Hidden",
@@ -41,7 +41,7 @@ pub fn property_tag_to_str(tag: &PropertyTag) -> &'static str {
     }
 }
 
-pub fn class_tag_to_str(tag: &ClassTag) -> &'static str {
+pub fn class_tag_to_str(tag: ClassTag) -> &'static str {
     match tag {
         ClassTag::Deprecated => "Deprecated",
         ClassTag::NotBrowsable => "NotBrowsable",
diff --git a/src/roblox/shared/classes.rs b/crates/lune-roblox/src/shared/classes.rs
similarity index 89%
rename from src/roblox/shared/classes.rs
rename to crates/lune-roblox/src/shared/classes.rs
index 90af83a..e87759b 100644
--- a/src/roblox/shared/classes.rs
+++ b/crates/lune-roblox/src/shared/classes.rs
@@ -2,7 +2,7 @@ use mlua::prelude::*;
 
 use rbx_dom_weak::types::Variant as DomValue;
 
-use crate::roblox::instance::Instance;
+use crate::instance::Instance;
 
 use super::instance::class_is_a;
 
@@ -20,8 +20,7 @@ pub(crate) fn add_class_restricted_getter<'lua, F: LuaUserDataFields<'lua, Insta
             field_getter(lua, this)
         } else {
             Err(LuaError::RuntimeError(format!(
-                "{} is not a valid member of {}",
-                field_name, class_name
+                "{field_name} is not a valid member of {class_name}",
             )))
         }
     });
@@ -42,8 +41,7 @@ pub(crate) fn add_class_restricted_setter<'lua, F: LuaUserDataFields<'lua, Insta
             field_getter(lua, this, value)
         } else {
             Err(LuaError::RuntimeError(format!(
-                "{} is not a valid member of {}",
-                field_name, class_name
+                "{field_name} is not a valid member of {class_name}",
             )))
         }
     });
@@ -64,8 +62,7 @@ pub(crate) fn add_class_restricted_method<'lua, M: LuaUserDataMethods<'lua, Inst
             method(lua, this, args)
         } else {
             Err(LuaError::RuntimeError(format!(
-                "{} is not a valid member of {}",
-                method_name, class_name
+                "{method_name} is not a valid member of {class_name}",
             )))
         }
     });
@@ -92,8 +89,7 @@ pub(crate) fn add_class_restricted_method_mut<
             method(lua, this, args)
         } else {
             Err(LuaError::RuntimeError(format!(
-                "{} is not a valid member of {}",
-                method_name, class_name
+                "{method_name} is not a valid member of {class_name}",
             )))
         }
     });
@@ -102,7 +98,7 @@ pub(crate) fn add_class_restricted_method_mut<
 /**
     Gets or creates the instance child with the given reference prop name and class name.
 
-    Note that the class name here must be an exact match, it is not checked using IsA.
+    Note that the class name here must be an exact match, it is not checked using `IsA`.
 
     The instance may be in one of several states but this function will guarantee that the
     property reference is correct and that the instance exists after it has been called:
diff --git a/src/roblox/shared/instance.rs b/crates/lune-roblox/src/shared/instance.rs
similarity index 98%
rename from src/roblox/shared/instance.rs
rename to crates/lune-roblox/src/shared/instance.rs
index a685ffb..c8b7c57 100644
--- a/src/roblox/shared/instance.rs
+++ b/crates/lune-roblox/src/shared/instance.rs
@@ -60,12 +60,12 @@ pub(crate) fn find_property_info(
                     value_type: Some(*value_type),
                     ..Default::default()
                 },
-                _ => Default::default(),
+                _ => PropertyInfo::default(),
             });
             break;
         } else if let Some(sup) = &class.superclass {
             // No property found, we should look at the superclass
-            class_name = Cow::Borrowed(sup)
+            class_name = Cow::Borrowed(sup);
         } else {
             break;
         }
@@ -87,7 +87,7 @@ pub(crate) fn find_property_info(
                 break;
             } else if let Some(sup) = &class.superclass {
                 // No default value found, we should look at the superclass
-                class_name = Cow::Borrowed(sup)
+                class_name = Cow::Borrowed(sup);
             } else {
                 break;
             }
diff --git a/src/roblox/shared/mod.rs b/crates/lune-roblox/src/shared/mod.rs
similarity index 100%
rename from src/roblox/shared/mod.rs
rename to crates/lune-roblox/src/shared/mod.rs
diff --git a/src/roblox/shared/userdata.rs b/crates/lune-roblox/src/shared/userdata.rs
similarity index 91%
rename from src/roblox/shared/userdata.rs
rename to crates/lune-roblox/src/shared/userdata.rs
index e43fb9a..9184625 100644
--- a/src/roblox/shared/userdata.rs
+++ b/crates/lune-roblox/src/shared/userdata.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::missing_errors_doc)]
+
 use std::{any::type_name, cell::RefCell, fmt, ops};
 
 use mlua::prelude::*;
@@ -5,21 +7,29 @@ use mlua::prelude::*;
 // Utility functions
 
 type ListWriter = dyn Fn(&mut fmt::Formatter<'_>, bool, &str) -> fmt::Result;
+
+#[must_use]
 pub fn make_list_writer() -> Box<ListWriter> {
     let first = RefCell::new(true);
     Box::new(move |f, flag, literal| {
         if flag {
             if first.take() {
-                write!(f, "{}", literal)?;
+                write!(f, "{literal}")?;
             } else {
-                write!(f, ", {}", literal)?;
+                write!(f, ", {literal}")?;
             }
         }
         Ok::<_, fmt::Error>(())
     })
 }
 
-// Userdata metamethod implementations
+/**
+    Userdata metamethod implementations
+
+    Note that many of these return [`LuaResult`] even though they don't
+    return any errors - this is for consistency reasons and to make it
+    easier to add these blanket implementations to [`LuaUserData`] impls.
+*/
 
 pub fn userdata_impl_to_string<D>(_: &Lua, datatype: &D, _: ()) -> LuaResult<String>
 where
diff --git a/crates/lune-std-datetime/Cargo.toml b/crates/lune-std-datetime/Cargo.toml
new file mode 100644
index 0000000..34bef78
--- /dev/null
+++ b/crates/lune-std-datetime/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "lune-std-datetime"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+
+thiserror = "1.0"
+chrono = "=0.4.34" # NOTE: 0.4.35 does not compile with chrono_lc
+chrono_lc = "0.1"
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
diff --git a/src/lune/builtins/datetime/mod.rs b/crates/lune-std-datetime/src/date_time.rs
similarity index 91%
rename from src/lune/builtins/datetime/mod.rs
rename to crates/lune-std-datetime/src/date_time.rs
index 87a07db..fbf3910 100644
--- a/src/lune/builtins/datetime/mod.rs
+++ b/crates/lune-std-datetime/src/date_time.rs
@@ -6,31 +6,8 @@ use chrono::prelude::*;
 use chrono::DateTime as ChronoDateTime;
 use chrono_lc::LocaleDate;
 
-use crate::lune::util::TableBuilder;
-
-mod error;
-mod values;
-
-use error::*;
-use values::*;
-
-pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
-    TableBuilder::new(lua)?
-        .with_function("fromIsoDate", |_, iso_date: String| {
-            Ok(DateTime::from_iso_date(iso_date)?)
-        })?
-        .with_function("fromLocalTime", |_, values| {
-            Ok(DateTime::from_local_time(&values)?)
-        })?
-        .with_function("fromUniversalTime", |_, values| {
-            Ok(DateTime::from_universal_time(&values)?)
-        })?
-        .with_function("fromUnixTimestamp", |_, timestamp| {
-            Ok(DateTime::from_unix_timestamp_float(timestamp)?)
-        })?
-        .with_function("now", |_, ()| Ok(DateTime::now()))?
-        .build_readonly()
-}
+use crate::result::{DateTimeError, DateTimeResult};
+use crate::values::DateTimeValues;
 
 const DEFAULT_FORMAT: &str = "%Y-%m-%d %H:%M:%S";
 const DEFAULT_LOCALE: &str = "en";
@@ -49,6 +26,7 @@ impl DateTime {
 
         See [`chrono::DateTime::now`] for additional details.
     */
+    #[must_use]
     pub fn now() -> Self {
         Self { inner: Utc::now() }
     }
@@ -66,6 +44,10 @@ impl DateTime {
         ```
 
         See [`chrono::DateTime::from_timestamp`] for additional details.
+
+        # Errors
+
+        Returns an error if the input value is out of range.
     */
     pub fn from_unix_timestamp_float(unix_timestamp: f64) -> DateTimeResult<Self> {
         let whole = unix_timestamp.trunc() as i64;
@@ -84,6 +66,10 @@ impl DateTime {
 
         See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
         for additional details and cases where this constructor may return an error.
+
+        # Errors
+
+        Returns an error if the date or time values are invalid.
     */
     pub fn from_universal_time(values: &DateTimeValues) -> DateTimeResult<Self> {
         let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
@@ -108,6 +94,10 @@ impl DateTime {
 
         See [`chrono::NaiveDate::from_ymd_opt`] and [`chrono::NaiveTime::from_hms_milli_opt`]
         for additional details and cases where this constructor may return an error.
+
+        # Errors
+
+        Returns an error if the date or time values are invalid or ambiguous.
     */
     pub fn from_local_time(values: &DateTimeValues) -> DateTimeResult<Self> {
         let date = NaiveDate::from_ymd_opt(values.year, values.month, values.day)
@@ -138,6 +128,7 @@ impl DateTime {
 
         See [`chrono_lc::DateTime::formatl`] for additional details.
     */
+    #[must_use]
     pub fn format_string_local(&self, format: Option<&str>, locale: Option<&str>) -> String {
         self.inner
             .with_timezone(&Local)
@@ -156,6 +147,7 @@ impl DateTime {
 
         See [`chrono_lc::DateTime::formatl`] for additional details.
     */
+    #[must_use]
     pub fn format_string_universal(&self, format: Option<&str>, locale: Option<&str>) -> String {
         self.inner
             .with_timezone(&Utc)
@@ -171,6 +163,10 @@ impl DateTime {
         `1996-12-19T16:39:57-08:00`, into a new `DateTime` struct.
 
         See [`chrono::DateTime::parse_from_rfc3339`] for additional details.
+
+        # Errors
+
+        Returns an error if the input string is not a valid RFC 3339 date-time.
     */
     pub fn from_iso_date(iso_date: impl AsRef<str>) -> DateTimeResult<Self> {
         let inner = ChronoDateTime::parse_from_rfc3339(iso_date.as_ref())?.with_timezone(&Utc);
@@ -181,6 +177,7 @@ impl DateTime {
         Extracts individual date & time values from this
         `DateTime`, using the current local time zone.
     */
+    #[must_use]
     pub fn to_local_time(self) -> DateTimeValues {
         DateTimeValues::from(self.inner.with_timezone(&Local))
     }
@@ -189,6 +186,7 @@ impl DateTime {
         Extracts individual date & time values from this
         `DateTime`, using the universal (UTC) time zone.
     */
+    #[must_use]
     pub fn to_universal_time(self) -> DateTimeValues {
         DateTimeValues::from(self.inner.with_timezone(&Utc))
     }
@@ -198,6 +196,7 @@ impl DateTime {
 
         See [`chrono::DateTime::to_rfc3339`] for additional details.
     */
+    #[must_use]
     pub fn to_iso_date(self) -> String {
         self.inner.to_rfc3339()
     }
diff --git a/crates/lune-std-datetime/src/lib.rs b/crates/lune-std-datetime/src/lib.rs
new file mode 100644
index 0000000..f53ddf3
--- /dev/null
+++ b/crates/lune-std-datetime/src/lib.rs
@@ -0,0 +1,36 @@
+#![allow(clippy::cargo_common_metadata)]
+
+use mlua::prelude::*;
+
+use lune_utils::TableBuilder;
+
+mod date_time;
+mod result;
+mod values;
+
+pub use self::date_time::DateTime;
+
+/**
+    Creates the `datetime` standard library module.
+
+    # Errors
+
+    Errors when out of memory.
+*/
+pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
+    TableBuilder::new(lua)?
+        .with_function("fromIsoDate", |_, iso_date: String| {
+            Ok(DateTime::from_iso_date(iso_date)?)
+        })?
+        .with_function("fromLocalTime", |_, values| {
+            Ok(DateTime::from_local_time(&values)?)
+        })?
+        .with_function("fromUniversalTime", |_, values| {
+            Ok(DateTime::from_universal_time(&values)?)
+        })?
+        .with_function("fromUnixTimestamp", |_, timestamp| {
+            Ok(DateTime::from_unix_timestamp_float(timestamp)?)
+        })?
+        .with_function("now", |_, ()| Ok(DateTime::now()))?
+        .build_readonly()
+}
diff --git a/src/lune/builtins/datetime/error.rs b/crates/lune-std-datetime/src/result.rs
similarity index 100%
rename from src/lune/builtins/datetime/error.rs
rename to crates/lune-std-datetime/src/result.rs
diff --git a/src/lune/builtins/datetime/values.rs b/crates/lune-std-datetime/src/values.rs
similarity index 92%
rename from src/lune/builtins/datetime/values.rs
rename to crates/lune-std-datetime/src/values.rs
index 7bca230..4193d63 100644
--- a/src/lune/builtins/datetime/values.rs
+++ b/crates/lune-std-datetime/src/values.rs
@@ -2,9 +2,9 @@ use mlua::prelude::*;
 
 use chrono::prelude::*;
 
-use crate::lune::util::TableBuilder;
+use lune_utils::TableBuilder;
 
-use super::error::{DateTimeError, DateTimeResult};
+use super::result::{DateTimeError, DateTimeResult};
 
 #[derive(Debug, Clone, Copy)]
 pub struct DateTimeValues {
@@ -61,9 +61,9 @@ where
 }
 
 /**
-    Conversion methods between DateTimeValues and plain lua tables
+    Conversion methods between `DateTimeValues` and plain lua tables
 
-    Note that the IntoLua implementation here uses a read-only table,
+    Note that the `IntoLua` implementation here uses a read-only table,
     since we generally want to convert into lua when we know we have
     a fixed point in time, and we guarantee that it doesn't change
 */
@@ -118,8 +118,8 @@ impl IntoLua<'_> for DateTimeValues {
 }
 
 /**
-    Conversion methods between chrono's timezone-aware DateTime to
-    and from our non-timezone-aware DateTimeValues values struct
+    Conversion methods between chrono's timezone-aware `DateTime` to
+    and from our non-timezone-aware `DateTimeValues` values struct
 */
 
 impl<T: TimeZone> From<DateTime<T>> for DateTimeValues {
diff --git a/crates/lune-std-fs/Cargo.toml b/crates/lune-std-fs/Cargo.toml
new file mode 100644
index 0000000..a3bc219
--- /dev/null
+++ b/crates/lune-std-fs/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "lune-std-fs"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+
+bstr = "1.9"
+
+tokio = { version = "1", default-features = false, features = ["fs"] }
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
+lune-std-datetime = { version = "0.1.0", path = "../lune-std-datetime" }
diff --git a/src/lune/builtins/fs/copy.rs b/crates/lune-std-fs/src/copy.rs
similarity index 96%
rename from src/lune/builtins/fs/copy.rs
rename to crates/lune-std-fs/src/copy.rs
index bcad01f..4fa3287 100644
--- a/src/lune/builtins/fs/copy.rs
+++ b/crates/lune-std-fs/src/copy.rs
@@ -13,7 +13,7 @@ pub struct CopyContents {
     pub files: Vec<(usize, PathBuf)>,
 }
 
-async fn get_contents_at(root: PathBuf, _options: FsWriteOptions) -> LuaResult<CopyContents> {
+async fn get_contents_at(root: PathBuf, _: FsWriteOptions) -> LuaResult<CopyContents> {
     let mut dirs = Vec::new();
     let mut files = Vec::new();
 
@@ -53,11 +53,11 @@ async fn get_contents_at(root: PathBuf, _options: FsWriteOptions) -> LuaResult<C
 
     // Ensure that all directory and file paths are relative to the root path
     // SAFETY: Since we only ever push dirs and files relative to the root, unwrap is safe
-    for (_, dir) in dirs.iter_mut() {
-        *dir = dir.strip_prefix(&normalized_root).unwrap().to_path_buf()
+    for (_, dir) in &mut dirs {
+        *dir = dir.strip_prefix(&normalized_root).unwrap().to_path_buf();
     }
-    for (_, file) in files.iter_mut() {
-        *file = file.strip_prefix(&normalized_root).unwrap().to_path_buf()
+    for (_, file) in &mut files {
+        *file = file.strip_prefix(&normalized_root).unwrap().to_path_buf();
     }
 
     // FUTURE: Deduplicate paths such that these directories:
diff --git a/src/lune/builtins/fs/mod.rs b/crates/lune-std-fs/src/lib.rs
similarity index 92%
rename from src/lune/builtins/fs/mod.rs
rename to crates/lune-std-fs/src/lib.rs
index 0db1f7f..954472a 100644
--- a/src/lune/builtins/fs/mod.rs
+++ b/crates/lune-std-fs/src/lib.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::cargo_common_metadata)]
+
 use std::io::ErrorKind as IoErrorKind;
 use std::path::{PathBuf, MAIN_SEPARATOR};
 
@@ -5,17 +7,24 @@ use bstr::{BString, ByteSlice};
 use mlua::prelude::*;
 use tokio::fs;
 
-use crate::lune::util::TableBuilder;
+use lune_utils::TableBuilder;
 
 mod copy;
 mod metadata;
 mod options;
 
-use copy::copy;
-use metadata::FsMetadata;
-use options::FsWriteOptions;
+use self::copy::copy;
+use self::metadata::FsMetadata;
+use self::options::FsWriteOptions;
 
-pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
+/**
+    Creates the `fs` standard library module.
+
+    # Errors
+
+    Errors when out of memory.
+*/
+pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
     TableBuilder::new(lua)?
         .with_async_function("readFile", fs_read_file)?
         .with_async_function("readDir", fs_read_dir)?
diff --git a/src/lune/builtins/fs/metadata.rs b/crates/lune-std-fs/src/metadata.rs
similarity index 98%
rename from src/lune/builtins/fs/metadata.rs
rename to crates/lune-std-fs/src/metadata.rs
index 93bdbe6..2cf01c8 100644
--- a/src/lune/builtins/fs/metadata.rs
+++ b/crates/lune-std-fs/src/metadata.rs
@@ -8,7 +8,7 @@ use std::{
 
 use mlua::prelude::*;
 
-use crate::lune::builtins::datetime::DateTime;
+use lune_std_datetime::DateTime;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum FsMetadataKind {
diff --git a/src/lune/builtins/fs/options.rs b/crates/lune-std-fs/src/options.rs
similarity index 100%
rename from src/lune/builtins/fs/options.rs
rename to crates/lune-std-fs/src/options.rs
diff --git a/crates/lune-std-luau/Cargo.toml b/crates/lune-std-luau/Cargo.toml
new file mode 100644
index 0000000..d01584c
--- /dev/null
+++ b/crates/lune-std-luau/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "lune-std-luau"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
diff --git a/src/lune/builtins/luau/mod.rs b/crates/lune-std-luau/src/lib.rs
similarity index 85%
rename from src/lune/builtins/luau/mod.rs
rename to crates/lune-std-luau/src/lib.rs
index 89ec972..e41eed5 100644
--- a/src/lune/builtins/luau/mod.rs
+++ b/crates/lune-std-luau/src/lib.rs
@@ -1,13 +1,23 @@
+#![allow(clippy::cargo_common_metadata)]
+
 use mlua::prelude::*;
 
-use crate::lune::util::TableBuilder;
+use lune_utils::TableBuilder;
 
 mod options;
-use options::{LuauCompileOptions, LuauLoadOptions};
+
+use self::options::{LuauCompileOptions, LuauLoadOptions};
 
 const BYTECODE_ERROR_BYTE: u8 = 0;
 
-pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
+/**
+    Creates the `luau` standard library module.
+
+    # Errors
+
+    Errors when out of memory.
+*/
+pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
     TableBuilder::new(lua)?
         .with_function("compile", compile_source)?
         .with_function("load", load_source)?
diff --git a/src/lune/builtins/luau/options.rs b/crates/lune-std-luau/src/options.rs
similarity index 95%
rename from src/lune/builtins/luau/options.rs
rename to crates/lune-std-luau/src/options.rs
index b34df9d..a2040ec 100644
--- a/src/lune/builtins/luau/options.rs
+++ b/crates/lune-std-luau/src/options.rs
@@ -1,8 +1,14 @@
+#![allow(clippy::struct_field_names)]
+
 use mlua::prelude::*;
 use mlua::Compiler as LuaCompiler;
 
 const DEFAULT_DEBUG_NAME: &str = "luau.load(...)";
 
+/**
+    Options for compiling Lua source code.
+*/
+#[derive(Debug, Clone, Copy)]
 pub struct LuauCompileOptions {
     pub(crate) optimization_level: u8,
     pub(crate) coverage_level: u8,
@@ -73,6 +79,10 @@ impl<'lua> FromLua<'lua> for LuauCompileOptions {
     }
 }
 
+/**
+    Options for loading Lua source code.
+*/
+#[derive(Debug, Clone)]
 pub struct LuauLoadOptions<'lua> {
     pub(crate) debug_name: String,
     pub(crate) environment: Option<LuaTable<'lua>>,
diff --git a/crates/lune-std-net/Cargo.toml b/crates/lune-std-net/Cargo.toml
new file mode 100644
index 0000000..966e374
--- /dev/null
+++ b/crates/lune-std-net/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "lune-std-net"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+mlua-luau-scheduler = "0.0.2"
+
+bstr = "1.9"
+futures-util = "0.3"
+hyper = { version = "1.1", features = ["full"] }
+hyper-util = { version = "0.1", features = ["full"] }
+http = "1.0"
+http-body-util = { version = "0.1" }
+hyper-tungstenite = { version = "0.13" }
+reqwest = { version = "0.11", default-features = false, features = [
+    "rustls-tls",
+] }
+tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
+urlencoding = "2.1"
+
+tokio = { version = "1", default-features = false, features = [
+    "sync",
+    "net",
+    "macros",
+] }
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
+lune-std-serde = { version = "0.1.0", path = "../lune-std-serde" }
diff --git a/src/lune/builtins/net/client.rs b/crates/lune-std-net/src/client.rs
similarity index 96%
rename from src/lune/builtins/net/client.rs
rename to crates/lune-std-net/src/client.rs
index 5eb2527..cae56bf 100644
--- a/src/lune/builtins/net/client.rs
+++ b/crates/lune-std-net/src/client.rs
@@ -4,10 +4,8 @@ use mlua::prelude::*;
 
 use reqwest::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_ENCODING};
 
-use crate::lune::{
-    builtins::serde::compress_decompress::{decompress, CompressDecompressFormat},
-    util::TableBuilder,
-};
+use lune_std_serde::{decompress, CompressDecompressFormat};
+use lune_utils::TableBuilder;
 
 use super::{config::RequestConfig, util::header_map_to_table};
 
@@ -103,7 +101,7 @@ impl NetClient {
                 .and_then(|(_, value)| value.to_str().ok())
                 .and_then(CompressDecompressFormat::detect_from_header_str);
             if let Some(format) = decompress_format {
-                res_bytes = decompress(format, res_bytes).await?;
+                res_bytes = decompress(res_bytes, format).await?;
                 res_decompressed = true;
             }
         }
diff --git a/src/lune/builtins/net/config.rs b/crates/lune-std-net/src/config.rs
similarity index 99%
rename from src/lune/builtins/net/config.rs
rename to crates/lune-std-net/src/config.rs
index 1abd121..6368d6d 100644
--- a/src/lune/builtins/net/config.rs
+++ b/crates/lune-std-net/src/config.rs
@@ -83,7 +83,7 @@ impl FromLua<'_> for RequestConfig {
                 query: HashMap::new(),
                 headers: HashMap::new(),
                 body: None,
-                options: Default::default(),
+                options: RequestConfigOptions::default(),
             })
         } else if let LuaValue::Table(tab) = value {
             // If we got a table we are able to configure the entire request
diff --git a/src/lune/builtins/net/mod.rs b/crates/lune-std-net/src/lib.rs
similarity index 83%
rename from src/lune/builtins/net/mod.rs
rename to crates/lune-std-net/src/lib.rs
index 9449e6b..3f42889 100644
--- a/src/lune/builtins/net/mod.rs
+++ b/crates/lune-std-net/src/lib.rs
@@ -1,4 +1,4 @@
-#![allow(unused_variables)]
+#![allow(clippy::cargo_common_metadata)]
 
 use bstr::BString;
 use mlua::prelude::*;
@@ -10,7 +10,7 @@ mod server;
 mod util;
 mod websocket;
 
-use crate::lune::util::TableBuilder;
+use lune_utils::TableBuilder;
 
 use self::{
     client::{NetClient, NetClientBuilder},
@@ -20,9 +20,16 @@ use self::{
     websocket::NetWebSocket,
 };
 
-use super::serde::encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat};
+use lune_std_serde::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat};
 
-pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
+/**
+    Creates the `net` standard library module.
+
+    # Errors
+
+    Errors when out of memory.
+*/
+pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
     NetClientBuilder::new()
         .headers(&[("User-Agent", create_user_agent_header(lua)?)])?
         .build()?
@@ -42,12 +49,13 @@ fn net_json_encode<'lua>(
     lua: &'lua Lua,
     (val, pretty): (LuaValue<'lua>, Option<bool>),
 ) -> LuaResult<LuaString<'lua>> {
-    EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default()))
-        .serialize_to_string(lua, val)
+    let config = EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default()));
+    encode(val, lua, config)
 }
 
 fn net_json_decode(lua: &Lua, json: BString) -> LuaResult<LuaValue> {
-    EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json)
+    let config = EncodeDecodeConfig::from(EncodeDecodeFormat::Json);
+    decode(json, lua, config)
 }
 
 async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult<LuaTable> {
diff --git a/src/lune/builtins/net/server/keys.rs b/crates/lune-std-net/src/server/keys.rs
similarity index 100%
rename from src/lune/builtins/net/server/keys.rs
rename to crates/lune-std-net/src/server/keys.rs
diff --git a/src/lune/builtins/net/server/mod.rs b/crates/lune-std-net/src/server/mod.rs
similarity index 94%
rename from src/lune/builtins/net/server/mod.rs
rename to crates/lune-std-net/src/server/mod.rs
index 5639bcb..7cfab9d 100644
--- a/src/lune/builtins/net/server/mod.rs
+++ b/crates/lune-std-net/src/server/mod.rs
@@ -10,7 +10,7 @@ use tokio::{net::TcpListener, pin};
 use mlua::prelude::*;
 use mlua_luau_scheduler::LuaSpawnExt;
 
-use crate::lune::util::TableBuilder;
+use lune_utils::TableBuilder;
 
 use super::config::ServeConfig;
 
@@ -81,7 +81,7 @@ pub async fn serve<'lua>(
 
             // Wait for either a new connection or a shutdown signal
             tokio::select! {
-                _ = fut_accept => {}
+                () = fut_accept => {}
                 res = fut_shutdown => {
                     // NOTE: We will only get a RecvError here if the serve handle is dropped,
                     // this means lua has garbage collected it and the user does not want
@@ -97,8 +97,8 @@ pub async fn serve<'lua>(
     TableBuilder::new(lua)?
         .with_value("ip", addr.ip().to_string())?
         .with_value("port", addr.port())?
-        .with_function("stop", move |lua, _: ()| match shutdown_tx.send(true) {
-            Ok(_) => Ok(()),
+        .with_function("stop", move |_, (): ()| match shutdown_tx.send(true) {
+            Ok(()) => Ok(()),
             Err(_) => Err(LuaError::runtime("Server already stopped")),
         })?
         .build_readonly()
diff --git a/src/lune/builtins/net/server/request.rs b/crates/lune-std-net/src/server/request.rs
similarity index 97%
rename from src/lune/builtins/net/server/request.rs
rename to crates/lune-std-net/src/server/request.rs
index bab7a5d..f3de802 100644
--- a/src/lune/builtins/net/server/request.rs
+++ b/crates/lune-std-net/src/server/request.rs
@@ -4,7 +4,7 @@ use http::request::Parts;
 
 use mlua::prelude::*;
 
-use crate::lune::util::TableBuilder;
+use lune_utils::TableBuilder;
 
 pub(super) struct LuaRequest {
     pub(super) _remote_addr: SocketAddr,
diff --git a/src/lune/builtins/net/server/response.rs b/crates/lune-std-net/src/server/response.rs
similarity index 100%
rename from src/lune/builtins/net/server/response.rs
rename to crates/lune-std-net/src/server/response.rs
diff --git a/src/lune/builtins/net/server/service.rs b/crates/lune-std-net/src/server/service.rs
similarity index 100%
rename from src/lune/builtins/net/server/service.rs
rename to crates/lune-std-net/src/server/service.rs
diff --git a/src/lune/builtins/net/util.rs b/crates/lune-std-net/src/util.rs
similarity index 97%
rename from src/lune/builtins/net/util.rs
rename to crates/lune-std-net/src/util.rs
index e18235e..ca79967 100644
--- a/src/lune/builtins/net/util.rs
+++ b/crates/lune-std-net/src/util.rs
@@ -5,7 +5,7 @@ use reqwest::header::HeaderMap;
 
 use mlua::prelude::*;
 
-use crate::lune::util::TableBuilder;
+use lune_utils::TableBuilder;
 
 pub fn create_user_agent_header(lua: &Lua) -> LuaResult<String> {
     let version_global = lua
@@ -28,7 +28,7 @@ pub fn header_map_to_table(
     remove_content_headers: bool,
 ) -> LuaResult<LuaTable> {
     let mut res_headers: HashMap<String, Vec<String>> = HashMap::new();
-    for (name, value) in headers.iter() {
+    for (name, value) in &headers {
         let name = name.as_str();
         let value = value.to_str().unwrap().to_owned();
         if let Some(existing) = res_headers.get_mut(name) {
diff --git a/src/lune/builtins/net/websocket.rs b/crates/lune-std-net/src/websocket.rs
similarity index 96%
rename from src/lune/builtins/net/websocket.rs
rename to crates/lune-std-net/src/websocket.rs
index 5dba4ec..ae2208a 100644
--- a/src/lune/builtins/net/websocket.rs
+++ b/crates/lune-std-net/src/websocket.rs
@@ -23,7 +23,7 @@ use hyper_tungstenite::{
     WebSocketStream,
 };
 
-use crate::lune::util::TableBuilder;
+use lune_utils::TableBuilder;
 
 // Wrapper implementation for compatibility and changing colon syntax to dot syntax
 const WEB_SOCKET_IMPL_LUA: &str = r#"
@@ -155,7 +155,7 @@ where
     }
 
     fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
-        methods.add_async_method("close", |lua, this, code: Option<u16>| async move {
+        methods.add_async_method("close", |_, this, code: Option<u16>| async move {
             this.close(code).await
         });
 
@@ -172,7 +172,7 @@ where
             },
         );
 
-        methods.add_async_method("next", |lua, this, _: ()| async move {
+        methods.add_async_method("next", |lua, this, (): ()| async move {
             let msg = this.next().await?;
 
             if let Some(WsMessage::Close(Some(frame))) = msg.as_ref() {
diff --git a/crates/lune-std-process/Cargo.toml b/crates/lune-std-process/Cargo.toml
new file mode 100644
index 0000000..211d875
--- /dev/null
+++ b/crates/lune-std-process/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "lune-std-process"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+mlua-luau-scheduler = "0.0.2"
+
+directories = "5.0"
+pin-project = "1.0"
+os_str_bytes = { version = "7.0", features = ["conversions"] }
+
+tokio = { version = "1", default-features = false, features = [
+    "sync",
+    "process",
+] }
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
diff --git a/src/lune/builtins/process/mod.rs b/crates/lune-std-process/src/lib.rs
similarity index 75%
rename from src/lune/builtins/process/mod.rs
rename to crates/lune-std-process/src/lib.rs
index b64e9aa..adc4eb8 100644
--- a/src/lune/builtins/process/mod.rs
+++ b/crates/lune-std-process/src/lib.rs
@@ -1,36 +1,49 @@
+#![allow(clippy::cargo_common_metadata)]
+
 use std::{
-    env::{self, consts},
-    path,
+    env::{
+        self,
+        consts::{ARCH, OS},
+    },
+    path::MAIN_SEPARATOR,
     process::Stdio,
 };
 
 use mlua::prelude::*;
+
+use lune_utils::TableBuilder;
 use mlua_luau_scheduler::{Functions, LuaSpawnExt};
 use os_str_bytes::RawOsString;
 use tokio::io::AsyncWriteExt;
 
-use crate::lune::util::{paths::CWD, TableBuilder};
-
-mod tee_writer;
-
 mod options;
-use options::ProcessSpawnOptions;
-
+mod tee_writer;
 mod wait_for_child;
-use wait_for_child::{wait_for_child, WaitForChildResult};
 
-pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
-    let cwd_str = {
-        let cwd_str = CWD.to_string_lossy().to_string();
-        if !cwd_str.ends_with(path::MAIN_SEPARATOR) {
-            format!("{cwd_str}{}", path::MAIN_SEPARATOR)
-        } else {
-            cwd_str
-        }
-    };
+use self::options::ProcessSpawnOptions;
+use self::wait_for_child::{wait_for_child, WaitForChildResult};
+
+use lune_utils::path::get_current_dir;
+
+/**
+    Creates the `process` standard library module.
+
+    # Errors
+
+    Errors when out of memory.
+*/
+#[allow(clippy::missing_panics_doc)]
+pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
+    let mut cwd_str = get_current_dir()
+        .to_str()
+        .expect("cwd should be valid UTF-8")
+        .to_string();
+    if !cwd_str.ends_with(MAIN_SEPARATOR) {
+        cwd_str.push(MAIN_SEPARATOR);
+    }
     // Create constants for OS & processor architecture
-    let os = lua.create_string(&consts::OS.to_lowercase())?;
-    let arch = lua.create_string(&consts::ARCH.to_lowercase())?;
+    let os = lua.create_string(&OS.to_lowercase())?;
+    let arch = lua.create_string(&ARCH.to_lowercase())?;
     // Create readonly args array
     let args_vec = lua
         .app_data_ref::<Vec<String>>()
@@ -94,33 +107,28 @@ fn process_env_set<'lua>(
         Err(LuaError::RuntimeError(
             "Key must not contain the NUL character".to_string(),
         ))
-    } else {
-        match value {
-            Some(value) => {
-                // Make sure value is valid, otherwise set_var will panic
-                if value.contains('\0') {
-                    Err(LuaError::RuntimeError(
-                        "Value must not contain the NUL character".to_string(),
-                    ))
-                } else {
-                    env::set_var(&key, &value);
-                    Ok(())
-                }
-            }
-            None => {
-                env::remove_var(&key);
-                Ok(())
-            }
+    } else if let Some(value) = value {
+        // Make sure value is valid, otherwise set_var will panic
+        if value.contains('\0') {
+            Err(LuaError::RuntimeError(
+                "Value must not contain the NUL character".to_string(),
+            ))
+        } else {
+            env::set_var(&key, &value);
+            Ok(())
         }
+    } else {
+        env::remove_var(&key);
+        Ok(())
     }
 }
 
 fn process_env_iter<'lua>(
     lua: &'lua Lua,
-    (_, _): (LuaValue<'lua>, ()),
+    (_, ()): (LuaValue<'lua>, ()),
 ) -> LuaResult<LuaFunction<'lua>> {
     let mut vars = env::vars_os().collect::<Vec<_>>().into_iter();
-    lua.create_function_mut(move |lua, _: ()| match vars.next() {
+    lua.create_function_mut(move |lua, (): ()| match vars.next() {
         Some((key, value)) => {
             let raw_key = RawOsString::new(key);
             let raw_value = RawOsString::new(value);
@@ -149,10 +157,10 @@ async fn process_spawn(
         An exit code may be missing if the process was terminated by
         some external signal, which is the only time we use this default
     */
-    let code = res.status.code().unwrap_or(match res.stderr.is_empty() {
-        true => 0,
-        false => 1,
-    });
+    let code = res
+        .status
+        .code()
+        .unwrap_or(i32::from(!res.stderr.is_empty()));
 
     // Construct and return a readonly lua table with results
     TableBuilder::new(lua)?
@@ -174,9 +182,10 @@ async fn spawn_command(
 
     let mut child = options
         .into_command(program, args)
-        .stdin(match stdin.is_some() {
-            true => Stdio::piped(),
-            false => Stdio::null(),
+        .stdin(if stdin.is_some() {
+            Stdio::piped()
+        } else {
+            Stdio::null()
         })
         .stdout(stdout.as_stdio())
         .stderr(stderr.as_stdio())
diff --git a/src/lune/builtins/process/options/kind.rs b/crates/lune-std-process/src/options/kind.rs
similarity index 98%
rename from src/lune/builtins/process/options/kind.rs
rename to crates/lune-std-process/src/options/kind.rs
index 3e0f39c..8eff17d 100644
--- a/src/lune/builtins/process/options/kind.rs
+++ b/crates/lune-std-process/src/options/kind.rs
@@ -1,6 +1,5 @@
 use std::{fmt, process::Stdio, str::FromStr};
 
-use itertools::Itertools;
 use mlua::prelude::*;
 
 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
@@ -55,6 +54,7 @@ impl FromStr for ProcessSpawnOptionsStdioKind {
                     ProcessSpawnOptionsStdioKind::all()
                         .iter()
                         .map(|k| format!("'{k}'"))
+                        .collect::<Vec<_>>()
                         .join(", ")
                 )))
             }
diff --git a/src/lune/builtins/process/options/mod.rs b/crates/lune-std-process/src/options/mod.rs
similarity index 98%
rename from src/lune/builtins/process/options/mod.rs
rename to crates/lune-std-process/src/options/mod.rs
index d37f844..8ef8be0 100644
--- a/src/lune/builtins/process/options/mod.rs
+++ b/crates/lune-std-process/src/options/mod.rs
@@ -56,7 +56,7 @@ impl<'lua> FromLua<'lua> for ProcessSpawnOptions {
                             "Invalid value for option 'cwd' - failed to get home directory",
                         )
                     })?;
-                    cwd = user_dirs.home_dir().join(stripped)
+                    cwd = user_dirs.home_dir().join(stripped);
                 }
                 if !cwd.exists() {
                     return Err(LuaError::runtime(
diff --git a/src/lune/builtins/process/options/stdio.rs b/crates/lune-std-process/src/options/stdio.rs
similarity index 100%
rename from src/lune/builtins/process/options/stdio.rs
rename to crates/lune-std-process/src/options/stdio.rs
diff --git a/src/lune/builtins/process/tee_writer.rs b/crates/lune-std-process/src/tee_writer.rs
similarity index 100%
rename from src/lune/builtins/process/tee_writer.rs
rename to crates/lune-std-process/src/tee_writer.rs
diff --git a/src/lune/builtins/process/wait_for_child.rs b/crates/lune-std-process/src/wait_for_child.rs
similarity index 94%
rename from src/lune/builtins/process/wait_for_child.rs
rename to crates/lune-std-process/src/wait_for_child.rs
index f126efe..4343041 100644
--- a/src/lune/builtins/process/wait_for_child.rs
+++ b/crates/lune-std-process/src/wait_for_child.rs
@@ -24,8 +24,7 @@ where
     R: AsyncRead + Unpin,
 {
     Ok(match kind {
-        ProcessSpawnOptionsStdioKind::None => Vec::new(),
-        ProcessSpawnOptionsStdioKind::Forward => Vec::new(),
+        ProcessSpawnOptionsStdioKind::None | ProcessSpawnOptionsStdioKind::Forward => Vec::new(),
         ProcessSpawnOptionsStdioKind::Default => {
             let mut read_from =
                 read_from.expect("read_from must be Some when stdio kind is Default");
diff --git a/crates/lune-std-regex/Cargo.toml b/crates/lune-std-regex/Cargo.toml
new file mode 100644
index 0000000..a7dc859
--- /dev/null
+++ b/crates/lune-std-regex/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "lune-std-regex"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+
+regex = "1.10"
+self_cell = "1.0"
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
diff --git a/src/lune/builtins/regex/captures.rs b/crates/lune-std-regex/src/captures.rs
similarity index 100%
rename from src/lune/builtins/regex/captures.rs
rename to crates/lune-std-regex/src/captures.rs
diff --git a/src/lune/builtins/regex/mod.rs b/crates/lune-std-regex/src/lib.rs
similarity index 56%
rename from src/lune/builtins/regex/mod.rs
rename to crates/lune-std-regex/src/lib.rs
index bb674c2..97fb279 100644
--- a/src/lune/builtins/regex/mod.rs
+++ b/crates/lune-std-regex/src/lib.rs
@@ -1,8 +1,8 @@
-#![allow(clippy::module_inception)]
+#![allow(clippy::cargo_common_metadata)]
 
 use mlua::prelude::*;
 
-use crate::lune::util::TableBuilder;
+use lune_utils::TableBuilder;
 
 mod captures;
 mod matches;
@@ -10,7 +10,14 @@ mod regex;
 
 use self::regex::LuaRegex;
 
-pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
+/**
+    Creates the `regex` standard library module.
+
+    # Errors
+
+    Errors when out of memory.
+*/
+pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
     TableBuilder::new(lua)?
         .with_function("new", new_regex)?
         .build_readonly()
diff --git a/src/lune/builtins/regex/matches.rs b/crates/lune-std-regex/src/matches.rs
similarity index 100%
rename from src/lune/builtins/regex/matches.rs
rename to crates/lune-std-regex/src/matches.rs
diff --git a/src/lune/builtins/regex/regex.rs b/crates/lune-std-regex/src/regex.rs
similarity index 98%
rename from src/lune/builtins/regex/regex.rs
rename to crates/lune-std-regex/src/regex.rs
index 3325e5d..9b83544 100644
--- a/src/lune/builtins/regex/regex.rs
+++ b/crates/lune-std-regex/src/regex.rs
@@ -46,7 +46,7 @@ impl LuaUserData for LuaRegex {
             Ok(this
                 .inner
                 .split(&text)
-                .map(|s| s.to_string())
+                .map(ToString::to_string)
                 .collect::<Vec<_>>())
         });
 
diff --git a/crates/lune-std-roblox/Cargo.toml b/crates/lune-std-roblox/Cargo.toml
new file mode 100644
index 0000000..af051c7
--- /dev/null
+++ b/crates/lune-std-roblox/Cargo.toml
@@ -0,0 +1,21 @@
+[package]
+name = "lune-std-roblox"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+mlua-luau-scheduler = "0.0.2"
+
+once_cell = "1.17"
+rbx_cookie = { version = "0.1.4", default-features = false }
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
+lune-roblox = { version = "0.1.0", path = "../lune-roblox" }
diff --git a/src/lune/builtins/roblox/mod.rs b/crates/lune-std-roblox/src/lib.rs
similarity index 83%
rename from src/lune/builtins/roblox/mod.rs
rename to crates/lune-std-roblox/src/lib.rs
index 308c148..d1f6a97 100644
--- a/src/lune/builtins/roblox/mod.rs
+++ b/crates/lune-std-roblox/src/lib.rs
@@ -1,23 +1,30 @@
+#![allow(clippy::cargo_common_metadata)]
+
 use mlua::prelude::*;
 use mlua_luau_scheduler::LuaSpawnExt;
 use once_cell::sync::OnceCell;
 
-use crate::{
-    lune::util::TableBuilder,
-    roblox::{
-        self,
-        document::{Document, DocumentError, DocumentFormat, DocumentKind},
-        instance::{registry::InstanceRegistry, Instance},
-        reflection::Database as ReflectionDatabase,
-    },
+use lune_roblox::{
+    document::{Document, DocumentError, DocumentFormat, DocumentKind},
+    instance::{registry::InstanceRegistry, Instance},
+    reflection::Database as ReflectionDatabase,
 };
 
 static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new();
 
-pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
+use lune_utils::TableBuilder;
+
+/**
+    Creates the `roblox` standard library module.
+
+    # Errors
+
+    Errors when out of memory.
+*/
+pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
     let mut roblox_constants = Vec::new();
 
-    let roblox_module = roblox::module(lua)?;
+    let roblox_module = lune_roblox::module(lua)?;
     for pair in roblox_module.pairs::<LuaValue, LuaValue>() {
         roblox_constants.push(pair?);
     }
@@ -116,16 +123,15 @@ fn implement_property(
         Option<LuaFunction>,
     ),
 ) -> LuaResult<()> {
-    let property_setter = match property_setter {
-        Some(setter) => setter,
-        None => {
-            let property_name = property_name.clone();
-            lua.create_function(move |_, _: LuaMultiValue| {
-                Err::<(), _>(LuaError::runtime(format!(
-                    "Property '{property_name}' is read-only"
-                )))
-            })?
-        }
+    let property_setter = if let Some(setter) = property_setter {
+        setter
+    } else {
+        let property_name = property_name.clone();
+        lua.create_function(move |_, _: LuaMultiValue| {
+            Err::<(), _>(LuaError::runtime(format!(
+                "Property '{property_name}' is read-only"
+            )))
+        })?
     };
     InstanceRegistry::insert_property_getter(lua, &class_name, &property_name, property_getter)
         .into_lua_err()?;
diff --git a/crates/lune-std-serde/Cargo.toml b/crates/lune-std-serde/Cargo.toml
new file mode 100644
index 0000000..4f8faef
--- /dev/null
+++ b/crates/lune-std-serde/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "lune-std-serde"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+
+async-compression = { version = "0.4", features = [
+    "tokio",
+    "brotli",
+    "deflate",
+    "gzip",
+    "zlib",
+] }
+bstr = "1.9"
+lz4 = "1.24"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = { version = "1.0", features = ["preserve_order"] }
+serde_yaml = "0.9"
+toml = { version = "0.8", features = ["preserve_order"] }
+
+tokio = { version = "1", default-features = false, features = [
+    "rt",
+    "io-util",
+] }
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
diff --git a/src/lune/builtins/serde/compress_decompress.rs b/crates/lune-std-serde/src/compress_decompress.rs
similarity index 66%
rename from src/lune/builtins/serde/compress_decompress.rs
rename to crates/lune-std-serde/src/compress_decompress.rs
index dac6ceb..3e5aaa0 100644
--- a/src/lune/builtins/serde/compress_decompress.rs
+++ b/crates/lune-std-serde/src/compress_decompress.rs
@@ -1,7 +1,12 @@
+use std::io::{copy as copy_std, Cursor, Read as _, Write as _};
+
 use mlua::prelude::*;
 
-use lz4_flex::{compress_prepend_size, decompress_size_prepended};
-use tokio::io::{copy, BufReader};
+use lz4::{Decoder, EncoderBuilder};
+use tokio::{
+    io::{copy, BufReader},
+    task::spawn_blocking,
+};
 
 use async_compression::{
     tokio::bufread::{
@@ -10,6 +15,9 @@ use async_compression::{
     Level::Best as CompressionQuality,
 };
 
+/**
+    A compression and decompression format supported by Lune.
+*/
 #[derive(Debug, Clone, Copy)]
 pub enum CompressDecompressFormat {
     Brotli,
@@ -20,6 +28,10 @@ pub enum CompressDecompressFormat {
 
 #[allow(dead_code)]
 impl CompressDecompressFormat {
+    /**
+        Detects a supported compression format from the given bytes.
+    */
+    #[allow(clippy::missing_panics_doc)]
     pub fn detect_from_bytes(bytes: impl AsRef<[u8]>) -> Option<Self> {
         match bytes.as_ref() {
             // https://github.com/PSeitz/lz4_flex/blob/main/src/frame/header.rs#L28
@@ -55,6 +67,11 @@ impl CompressDecompressFormat {
         }
     }
 
+    /**
+        Detects a supported compression format from the given header string.
+
+        The given header script should be a valid `Content-Encoding` header value.
+    */
     pub fn detect_from_header_str(header: impl AsRef<str>) -> Option<Self> {
         // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding#directives
         match header.as_ref().to_ascii_lowercase().trim() {
@@ -92,13 +109,23 @@ impl<'lua> FromLua<'lua> for CompressDecompressFormat {
     }
 }
 
+/**
+    Compresses the given bytes using the specified format.
+
+    # Errors
+
+    Errors when the compression fails.
+*/
 pub async fn compress<'lua>(
-    format: CompressDecompressFormat,
     source: impl AsRef<[u8]>,
+    format: CompressDecompressFormat,
 ) -> LuaResult<Vec<u8>> {
     if let CompressDecompressFormat::LZ4 = format {
         let source = source.as_ref().to_vec();
-        return Ok(blocking::unblock(move || compress_prepend_size(&source)).await);
+        return spawn_blocking(move || compress_lz4(source))
+            .await
+            .into_lua_err()?
+            .into_lua_err();
     }
 
     let mut bytes = Vec::new();
@@ -123,14 +150,22 @@ pub async fn compress<'lua>(
     Ok(bytes)
 }
 
+/**
+    Decompresses the given bytes using the specified format.
+
+    # Errors
+
+    Errors when the decompression fails.
+*/
 pub async fn decompress<'lua>(
-    format: CompressDecompressFormat,
     source: impl AsRef<[u8]>,
+    format: CompressDecompressFormat,
 ) -> LuaResult<Vec<u8>> {
     if let CompressDecompressFormat::LZ4 = format {
         let source = source.as_ref().to_vec();
-        return blocking::unblock(move || decompress_size_prepended(&source))
+        return spawn_blocking(move || decompress_lz4(source))
             .await
+            .into_lua_err()?
             .into_lua_err();
     }
 
@@ -155,3 +190,47 @@ pub async fn decompress<'lua>(
 
     Ok(bytes)
 }
+
+// TODO: Remove the compatibility layer. Prepending size is no longer
+// necessary, using lz4 create instead of lz4-flex, but we must remove
+// it in a major version to not unexpectedly break compatibility
+
+fn compress_lz4(input: Vec<u8>) -> LuaResult<Vec<u8>> {
+    let mut input = Cursor::new(input);
+    let mut output = Cursor::new(Vec::new());
+
+    // Prepend size for compatibility with old lz4-flex implementation
+    let len = input.get_ref().len() as u32;
+    output.write_all(len.to_le_bytes().as_ref())?;
+
+    let mut encoder = EncoderBuilder::new()
+        .level(16)
+        .checksum(lz4::ContentChecksum::ChecksumEnabled)
+        .block_mode(lz4::BlockMode::Independent)
+        .build(output)?;
+
+    copy_std(&mut input, &mut encoder)?;
+    let (output, result) = encoder.finish();
+    result?;
+
+    Ok(output.into_inner())
+}
+
+fn decompress_lz4(input: Vec<u8>) -> LuaResult<Vec<u8>> {
+    let mut input = Cursor::new(input);
+
+    // Skip size for compatibility with old lz4-flex implementation
+    // Note that right now we use it for preallocating the output buffer
+    // and a small efficiency gain, maybe we can expose this as some kind
+    // of "size hint" parameter instead in the serde library in the future
+    let mut size = [0; 4];
+    input.read_exact(&mut size)?;
+
+    let capacity = u32::from_le_bytes(size) as usize;
+    let mut output = Cursor::new(Vec::with_capacity(capacity));
+
+    let mut decoder = Decoder::new(input)?;
+    copy_std(&mut decoder, &mut output)?;
+
+    Ok(output.into_inner())
+}
diff --git a/crates/lune-std-serde/src/encode_decode.rs b/crates/lune-std-serde/src/encode_decode.rs
new file mode 100644
index 0000000..80e1a5f
--- /dev/null
+++ b/crates/lune-std-serde/src/encode_decode.rs
@@ -0,0 +1,158 @@
+use mlua::prelude::*;
+
+use serde_json::Value as JsonValue;
+use serde_yaml::Value as YamlValue;
+use toml::Value as TomlValue;
+
+// NOTE: These are options for going from other format -> lua ("serializing" lua values)
+const LUA_SERIALIZE_OPTIONS: LuaSerializeOptions = LuaSerializeOptions::new()
+    .set_array_metatable(false)
+    .serialize_none_to_null(false)
+    .serialize_unit_to_null(false);
+
+// NOTE: These are options for going from lua -> other format ("deserializing" lua values)
+const LUA_DESERIALIZE_OPTIONS: LuaDeserializeOptions = LuaDeserializeOptions::new()
+    .sort_keys(true)
+    .deny_recursive_tables(false)
+    .deny_unsupported_types(true);
+
+/**
+    An encoding and decoding format supported by Lune.
+
+    Encode / decode in this case is synonymous with serialize / deserialize.
+*/
+#[derive(Debug, Clone, Copy)]
+pub enum EncodeDecodeFormat {
+    Json,
+    Yaml,
+    Toml,
+}
+
+impl<'lua> FromLua<'lua> for EncodeDecodeFormat {
+    fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
+        if let LuaValue::String(s) = &value {
+            match s.to_string_lossy().to_ascii_lowercase().trim() {
+                "json" => Ok(Self::Json),
+                "yaml" => Ok(Self::Yaml),
+                "toml" => Ok(Self::Toml),
+                kind => Err(LuaError::FromLuaConversionError {
+                    from: value.type_name(),
+                    to: "EncodeDecodeFormat",
+                    message: Some(format!(
+                        "Invalid format '{kind}', valid formats are:  json, yaml, toml"
+                    )),
+                }),
+            }
+        } else {
+            Err(LuaError::FromLuaConversionError {
+                from: value.type_name(),
+                to: "EncodeDecodeFormat",
+                message: None,
+            })
+        }
+    }
+}
+
+/**
+    Configuration for encoding and decoding values.
+
+    Encoding / decoding in this case is synonymous with serialize / deserialize.
+*/
+#[derive(Debug, Clone, Copy)]
+pub struct EncodeDecodeConfig {
+    pub format: EncodeDecodeFormat,
+    pub pretty: bool,
+}
+
+impl From<EncodeDecodeFormat> for EncodeDecodeConfig {
+    fn from(format: EncodeDecodeFormat) -> Self {
+        Self {
+            format,
+            pretty: false,
+        }
+    }
+}
+
+impl From<(EncodeDecodeFormat, bool)> for EncodeDecodeConfig {
+    fn from(value: (EncodeDecodeFormat, bool)) -> Self {
+        Self {
+            format: value.0,
+            pretty: value.1,
+        }
+    }
+}
+
+/**
+    Encodes / serializes the given value into a string, using the specified configuration.
+
+    # Errors
+
+    Errors when the encoding fails.
+*/
+pub fn encode<'lua>(
+    value: LuaValue<'lua>,
+    lua: &'lua Lua,
+    config: EncodeDecodeConfig,
+) -> LuaResult<LuaString<'lua>> {
+    let bytes = match config.format {
+        EncodeDecodeFormat::Json => {
+            let serialized: JsonValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
+            if config.pretty {
+                serde_json::to_vec_pretty(&serialized).into_lua_err()?
+            } else {
+                serde_json::to_vec(&serialized).into_lua_err()?
+            }
+        }
+        EncodeDecodeFormat::Yaml => {
+            let serialized: YamlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
+            let mut writer = Vec::with_capacity(128);
+            serde_yaml::to_writer(&mut writer, &serialized).into_lua_err()?;
+            writer
+        }
+        EncodeDecodeFormat::Toml => {
+            let serialized: TomlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
+            let s = if config.pretty {
+                toml::to_string_pretty(&serialized).into_lua_err()?
+            } else {
+                toml::to_string(&serialized).into_lua_err()?
+            };
+            s.as_bytes().to_vec()
+        }
+    };
+    lua.create_string(bytes)
+}
+
+/**
+    Decodes / deserializes the given string into a value, using the specified configuration.
+
+    # Errors
+
+    Errors when the decoding fails.
+*/
+pub fn decode(
+    bytes: impl AsRef<[u8]>,
+    lua: &Lua,
+    config: EncodeDecodeConfig,
+) -> LuaResult<LuaValue> {
+    let bytes = bytes.as_ref();
+    match config.format {
+        EncodeDecodeFormat::Json => {
+            let value: JsonValue = serde_json::from_slice(bytes).into_lua_err()?;
+            lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
+        }
+        EncodeDecodeFormat::Yaml => {
+            let value: YamlValue = serde_yaml::from_slice(bytes).into_lua_err()?;
+            lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
+        }
+        EncodeDecodeFormat::Toml => {
+            if let Ok(s) = String::from_utf8(bytes.to_vec()) {
+                let value: TomlValue = toml::from_str(&s).into_lua_err()?;
+                lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
+            } else {
+                Err(LuaError::RuntimeError(
+                    "TOML must be valid utf-8".to_string(),
+                ))
+            }
+        }
+    }
+}
diff --git a/crates/lune-std-serde/src/lib.rs b/crates/lune-std-serde/src/lib.rs
new file mode 100644
index 0000000..4514a75
--- /dev/null
+++ b/crates/lune-std-serde/src/lib.rs
@@ -0,0 +1,57 @@
+#![allow(clippy::cargo_common_metadata)]
+
+use bstr::BString;
+use mlua::prelude::*;
+
+use lune_utils::TableBuilder;
+
+mod compress_decompress;
+mod encode_decode;
+
+pub use self::compress_decompress::{compress, decompress, CompressDecompressFormat};
+pub use self::encode_decode::{decode, encode, EncodeDecodeConfig, EncodeDecodeFormat};
+
+/**
+    Creates the `serde` standard library module.
+
+    # Errors
+
+    Errors when out of memory.
+*/
+pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
+    TableBuilder::new(lua)?
+        .with_function("encode", serde_encode)?
+        .with_function("decode", serde_decode)?
+        .with_async_function("compress", serde_compress)?
+        .with_async_function("decompress", serde_decompress)?
+        .build_readonly()
+}
+
+fn serde_encode<'lua>(
+    lua: &'lua Lua,
+    (format, value, pretty): (EncodeDecodeFormat, LuaValue<'lua>, Option<bool>),
+) -> LuaResult<LuaString<'lua>> {
+    let config = EncodeDecodeConfig::from((format, pretty.unwrap_or_default()));
+    encode(value, lua, config)
+}
+
+fn serde_decode(lua: &Lua, (format, bs): (EncodeDecodeFormat, BString)) -> LuaResult<LuaValue> {
+    let config = EncodeDecodeConfig::from(format);
+    decode(bs, lua, config)
+}
+
+async fn serde_compress(
+    lua: &Lua,
+    (format, bs): (CompressDecompressFormat, BString),
+) -> LuaResult<LuaString> {
+    let bytes = compress(bs, format).await?;
+    lua.create_string(bytes)
+}
+
+async fn serde_decompress(
+    lua: &Lua,
+    (format, bs): (CompressDecompressFormat, BString),
+) -> LuaResult<LuaString> {
+    let bytes = decompress(bs, format).await?;
+    lua.create_string(bytes)
+}
diff --git a/crates/lune-std-stdio/Cargo.toml b/crates/lune-std-stdio/Cargo.toml
new file mode 100644
index 0000000..464f5d9
--- /dev/null
+++ b/crates/lune-std-stdio/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "lune-std-stdio"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+dialoguer = "0.11"
+mlua = { version = "0.9.7", features = ["luau"] }
+mlua-luau-scheduler = "0.0.2"
+
+tokio = { version = "1", default-features = false, features = [
+    "io-std",
+    "io-util",
+] }
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
diff --git a/crates/lune-std-stdio/src/lib.rs b/crates/lune-std-stdio/src/lib.rs
new file mode 100644
index 0000000..a4f7751
--- /dev/null
+++ b/crates/lune-std-stdio/src/lib.rs
@@ -0,0 +1,85 @@
+#![allow(clippy::cargo_common_metadata)]
+
+use lune_utils::fmt::{pretty_format_multi_value, ValueFormatConfig};
+use mlua::prelude::*;
+use mlua_luau_scheduler::LuaSpawnExt;
+
+use tokio::io::{stderr, stdin, stdout, AsyncReadExt, AsyncWriteExt};
+
+use lune_utils::TableBuilder;
+
+mod prompt;
+mod style_and_color;
+
+use self::prompt::{prompt, PromptOptions, PromptResult};
+use self::style_and_color::{ColorKind, StyleKind};
+
+const FORMAT_CONFIG: ValueFormatConfig = ValueFormatConfig::new()
+    .with_max_depth(4)
+    .with_colors_enabled(false);
+
+/**
+    Creates the `stdio` standard library module.
+
+    # Errors
+
+    Errors when out of memory.
+*/
+pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
+    TableBuilder::new(lua)?
+        .with_function("color", stdio_color)?
+        .with_function("style", stdio_style)?
+        .with_function("format", stdio_format)?
+        .with_async_function("write", stdio_write)?
+        .with_async_function("ewrite", stdio_ewrite)?
+        .with_async_function("readToEnd", stdio_read_to_end)?
+        .with_async_function("prompt", stdio_prompt)?
+        .build_readonly()
+}
+
+fn stdio_color(lua: &Lua, color: ColorKind) -> LuaResult<LuaValue> {
+    color.ansi_escape_sequence().into_lua(lua)
+}
+
+fn stdio_style(lua: &Lua, style: StyleKind) -> LuaResult<LuaValue> {
+    style.ansi_escape_sequence().into_lua(lua)
+}
+
+fn stdio_format(_: &Lua, args: LuaMultiValue) -> LuaResult<String> {
+    Ok(pretty_format_multi_value(&args, &FORMAT_CONFIG))
+}
+
+async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
+    let mut stdout = stdout();
+    stdout.write_all(s.as_bytes()).await?;
+    stdout.flush().await?;
+    Ok(())
+}
+
+async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
+    let mut stderr = stderr();
+    stderr.write_all(s.as_bytes()).await?;
+    stderr.flush().await?;
+    Ok(())
+}
+
+/*
+    FUTURE: Figure out how to expose some kind of "readLine" function using a buffered reader.
+
+    This is a bit tricky since we would want to be able to use **both** readLine and readToEnd
+    in the same script, doing something like readLine, readLine, readToEnd from lua, and
+    having that capture the first two lines and then read the rest of the input.
+*/
+
+async fn stdio_read_to_end(lua: &Lua, (): ()) -> LuaResult<LuaString> {
+    let mut input = Vec::new();
+    let mut stdin = stdin();
+    stdin.read_to_end(&mut input).await?;
+    lua.create_string(&input)
+}
+
+async fn stdio_prompt(lua: &Lua, options: PromptOptions) -> LuaResult<PromptResult> {
+    lua.spawn_blocking(move || prompt(options))
+        .await
+        .into_lua_err()
+}
diff --git a/src/lune/builtins/stdio/prompt.rs b/crates/lune-std-stdio/src/prompt.rs
similarity index 60%
rename from src/lune/builtins/stdio/prompt.rs
rename to crates/lune-std-stdio/src/prompt.rs
index 9cdd899..e1fcc02 100644
--- a/src/lune/builtins/stdio/prompt.rs
+++ b/crates/lune-std-stdio/src/prompt.rs
@@ -1,5 +1,6 @@
-use std::fmt;
+use std::{fmt, str::FromStr};
 
+use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select};
 use mlua::prelude::*;
 
 #[derive(Debug, Clone, Copy)]
@@ -11,9 +12,7 @@ pub enum PromptKind {
 }
 
 impl PromptKind {
-    fn get_all() -> Vec<Self> {
-        vec![Self::Text, Self::Confirm, Self::Select, Self::MultiSelect]
-    }
+    const ALL: [PromptKind; 4] = [Self::Text, Self::Confirm, Self::Select, Self::MultiSelect];
 }
 
 impl Default for PromptKind {
@@ -22,6 +21,19 @@ impl Default for PromptKind {
     }
 }
 
+impl FromStr for PromptKind {
+    type Err = ();
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s.trim().to_ascii_lowercase().as_str() {
+            "text" => Ok(Self::Text),
+            "confirm" => Ok(Self::Confirm),
+            "select" => Ok(Self::Select),
+            "multiselect" => Ok(Self::MultiSelect),
+            _ => Err(()),
+        }
+    }
+}
+
 impl fmt::Display for PromptKind {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(
@@ -43,45 +55,18 @@ impl<'lua> FromLua<'lua> for PromptKind {
             Ok(Self::default())
         } else if let LuaValue::String(s) = value {
             let s = s.to_str()?;
-            /*
-                If the user only typed the prompt kind slightly wrong, meaning
-                it has some kind of space in it, a weird character, or an uppercase
-                character, we should try to be permissive as possible and still work
-
-                Not everyone is using an IDE with proper Luau type definitions
-                installed, and Luau is still a permissive scripting language
-                even though it has a strict (but optional) type system
-            */
-            let s = s
-                .chars()
-                .filter_map(|c| {
-                    if c.is_ascii_alphabetic() {
-                        Some(c.to_ascii_lowercase())
-                    } else {
-                        None
-                    }
-                })
-                .collect::<String>();
-            // If the prompt kind is still invalid we will
-            // show the user a descriptive error message
-            match s.as_ref() {
-                "text" => Ok(Self::Text),
-                "confirm" => Ok(Self::Confirm),
-                "select" => Ok(Self::Select),
-                "multiselect" => Ok(Self::MultiSelect),
-                s => Err(LuaError::FromLuaConversionError {
-                    from: "string",
-                    to: "PromptKind",
-                    message: Some(format!(
-                        "Invalid prompt kind '{s}', valid kinds are:\n{}",
-                        PromptKind::get_all()
-                            .iter()
-                            .map(ToString::to_string)
-                            .collect::<Vec<_>>()
-                            .join(", ")
-                    )),
-                }),
-            }
+            s.parse().map_err(|()| LuaError::FromLuaConversionError {
+                from: "string",
+                to: "PromptKind",
+                message: Some(format!(
+                    "Invalid prompt kind '{s}', valid kinds are:\n{}",
+                    PromptKind::ALL
+                        .iter()
+                        .map(ToString::to_string)
+                        .collect::<Vec<_>>()
+                        .join(", ")
+                )),
+            })
         } else {
             Err(LuaError::FromLuaConversionError {
                 from: "nil",
@@ -163,8 +148,8 @@ impl<'lua> FromLuaMulti<'lua> for PromptOptions {
         Ok(Self {
             kind,
             text,
-            default_bool,
             default_string,
+            default_bool,
             options,
         })
     }
@@ -190,3 +175,53 @@ impl<'lua> IntoLua<'lua> for PromptResult {
         })
     }
 }
+
+pub fn prompt(options: PromptOptions) -> LuaResult<PromptResult> {
+    let theme = ColorfulTheme::default();
+    match options.kind {
+        PromptKind::Text => {
+            let input: String = Input::with_theme(&theme)
+                .allow_empty(true)
+                .with_prompt(options.text.unwrap_or_default())
+                .with_initial_text(options.default_string.unwrap_or_default())
+                .interact_text()
+                .into_lua_err()?;
+            Ok(PromptResult::String(input))
+        }
+        PromptKind::Confirm => {
+            let mut prompt = Confirm::with_theme(&theme);
+            if let Some(b) = options.default_bool {
+                prompt = prompt.default(b);
+            };
+            let result = prompt
+                .with_prompt(&options.text.expect("Missing text in prompt options"))
+                .interact()
+                .into_lua_err()?;
+            Ok(PromptResult::Boolean(result))
+        }
+        PromptKind::Select => {
+            let chosen = Select::with_theme(&theme)
+                .with_prompt(&options.text.unwrap_or_default())
+                .items(&options.options.expect("Missing options in prompt options"))
+                .interact_opt()
+                .into_lua_err()?;
+            Ok(match chosen {
+                Some(idx) => PromptResult::Index(idx + 1),
+                None => PromptResult::None,
+            })
+        }
+        PromptKind::MultiSelect => {
+            let chosen = MultiSelect::with_theme(&theme)
+                .with_prompt(&options.text.unwrap_or_default())
+                .items(&options.options.expect("Missing options in prompt options"))
+                .interact_opt()
+                .into_lua_err()?;
+            Ok(match chosen {
+                None => PromptResult::None,
+                Some(indices) => {
+                    PromptResult::Indices(indices.iter().map(|idx| *idx + 1).collect())
+                }
+            })
+        }
+    }
+}
diff --git a/crates/lune-std-stdio/src/style_and_color.rs b/crates/lune-std-stdio/src/style_and_color.rs
new file mode 100644
index 0000000..4080118
--- /dev/null
+++ b/crates/lune-std-stdio/src/style_and_color.rs
@@ -0,0 +1,195 @@
+use std::str::FromStr;
+
+use mlua::prelude::*;
+
+const ESCAPE_SEQ_RESET: &str = "\x1b[0m";
+
+/**
+    A color kind supported by the `stdio` standard library.
+*/
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ColorKind {
+    Reset,
+    Black,
+    Red,
+    Green,
+    Yellow,
+    Blue,
+    Magenta,
+    Cyan,
+    White,
+}
+
+impl ColorKind {
+    pub const ALL: [Self; 9] = [
+        Self::Reset,
+        Self::Black,
+        Self::Red,
+        Self::Green,
+        Self::Yellow,
+        Self::Blue,
+        Self::Magenta,
+        Self::Cyan,
+        Self::White,
+    ];
+
+    /**
+        Returns the human-friendly name of this color kind.
+    */
+    pub fn name(self) -> &'static str {
+        match self {
+            Self::Reset => "reset",
+            Self::Black => "black",
+            Self::Red => "red",
+            Self::Green => "green",
+            Self::Yellow => "yellow",
+            Self::Blue => "blue",
+            Self::Magenta => "magenta",
+            Self::Cyan => "cyan",
+            Self::White => "white",
+        }
+    }
+
+    /**
+        Returns the ANSI escape sequence for the color kind.
+    */
+    pub fn ansi_escape_sequence(self) -> &'static str {
+        match self {
+            Self::Reset => ESCAPE_SEQ_RESET,
+            Self::Black => "\x1b[30m",
+            Self::Red => "\x1b[31m",
+            Self::Green => "\x1b[32m",
+            Self::Yellow => "\x1b[33m",
+            Self::Blue => "\x1b[34m",
+            Self::Magenta => "\x1b[35m",
+            Self::Cyan => "\x1b[36m",
+            Self::White => "\x1b[37m",
+        }
+    }
+}
+
+impl FromStr for ColorKind {
+    type Err = ();
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(match s.trim().to_ascii_lowercase().as_str() {
+            "reset" => Self::Reset,
+            "black" => Self::Black,
+            "red" => Self::Red,
+            "green" => Self::Green,
+            "yellow" => Self::Yellow,
+            "blue" => Self::Blue,
+            // NOTE: Previous versions of Lune had this color as "purple" instead
+            // of "magenta", so we keep this here for backwards compatibility.
+            "magenta" | "purple" => Self::Magenta,
+            "cyan" => Self::Cyan,
+            "white" => Self::White,
+            _ => return Err(()),
+        })
+    }
+}
+
+impl FromLua<'_> for ColorKind {
+    fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
+        if let LuaValue::String(s) = value {
+            let s = s.to_str()?;
+            match s.parse() {
+                Ok(color) => Ok(color),
+                Err(()) => Err(LuaError::FromLuaConversionError {
+                    from: "string",
+                    to: "ColorKind",
+                    message: Some(format!(
+                        "Invalid color kind '{s}'\nValid kinds are: {}",
+                        Self::ALL
+                            .iter()
+                            .map(|kind| kind.name())
+                            .collect::<Vec<_>>()
+                            .join(", ")
+                    )),
+                }),
+            }
+        } else {
+            Err(LuaError::FromLuaConversionError {
+                from: value.type_name(),
+                to: "ColorKind",
+                message: None,
+            })
+        }
+    }
+}
+
+/**
+    A style kind supported by the `stdio` standard library.
+*/
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum StyleKind {
+    Reset,
+    Bold,
+    Dim,
+}
+
+impl StyleKind {
+    pub const ALL: [Self; 3] = [Self::Reset, Self::Bold, Self::Dim];
+
+    /**
+        Returns the human-friendly name for this style kind.
+    */
+    pub fn name(self) -> &'static str {
+        match self {
+            Self::Reset => "reset",
+            Self::Bold => "bold",
+            Self::Dim => "dim",
+        }
+    }
+
+    /**
+        Returns the ANSI escape sequence for this style kind.
+    */
+    pub fn ansi_escape_sequence(self) -> &'static str {
+        match self {
+            Self::Reset => ESCAPE_SEQ_RESET,
+            Self::Bold => "\x1b[1m",
+            Self::Dim => "\x1b[2m",
+        }
+    }
+}
+
+impl FromStr for StyleKind {
+    type Err = ();
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(match s.trim().to_ascii_lowercase().as_str() {
+            "reset" => Self::Reset,
+            "bold" => Self::Bold,
+            "dim" => Self::Dim,
+            _ => return Err(()),
+        })
+    }
+}
+
+impl FromLua<'_> for StyleKind {
+    fn from_lua(value: LuaValue, _: &Lua) -> LuaResult<Self> {
+        if let LuaValue::String(s) = value {
+            let s = s.to_str()?;
+            match s.parse() {
+                Ok(style) => Ok(style),
+                Err(()) => Err(LuaError::FromLuaConversionError {
+                    from: "string",
+                    to: "StyleKind",
+                    message: Some(format!(
+                        "Invalid style kind '{s}'\nValid kinds are: {}",
+                        Self::ALL
+                            .iter()
+                            .map(|kind| kind.name())
+                            .collect::<Vec<_>>()
+                            .join(", ")
+                    )),
+                }),
+            }
+        } else {
+            Err(LuaError::FromLuaConversionError {
+                from: value.type_name(),
+                to: "StyleKind",
+                message: None,
+            })
+        }
+    }
+}
diff --git a/crates/lune-std-task/Cargo.toml b/crates/lune-std-task/Cargo.toml
new file mode 100644
index 0000000..4df2d8a
--- /dev/null
+++ b/crates/lune-std-task/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "lune-std-task"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+mlua-luau-scheduler = "0.0.2"
+
+tokio = { version = "1", default-features = false, features = ["time"] }
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
diff --git a/src/lune/builtins/task/mod.rs b/crates/lune-std-task/src/lib.rs
similarity index 81%
rename from src/lune/builtins/task/mod.rs
rename to crates/lune-std-task/src/lib.rs
index 94a4da0..47a78d5 100644
--- a/src/lune/builtins/task/mod.rs
+++ b/crates/lune-std-task/src/lib.rs
@@ -1,20 +1,22 @@
+#![allow(clippy::cargo_common_metadata)]
+
 use std::time::Duration;
 
 use mlua::prelude::*;
-
 use mlua_luau_scheduler::Functions;
-use tokio::time::{self, Instant};
 
-use crate::lune::util::TableBuilder;
+use tokio::time::{sleep, Instant};
 
-const DELAY_IMPL_LUA: &str = r#"
-return defer(function(...)
-    wait(select(1, ...))
-    spawn(select(2, ...))
-end, ...)
-"#;
+use lune_utils::TableBuilder;
 
-pub fn create(lua: &Lua) -> LuaResult<LuaTable<'_>> {
+/**
+    Creates the `task` standard library module.
+
+    # Errors
+
+    Errors when out of memory, or if default Lua globals are missing.
+*/
+pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
     let fns = Functions::new(lua)?;
 
     // Create wait & delay functions
@@ -46,11 +48,18 @@ pub fn create(lua: &Lua) -> LuaResult<LuaTable<'_>> {
         .build_readonly()
 }
 
+const DELAY_IMPL_LUA: &str = r"
+return defer(function(...)
+    wait(select(1, ...))
+    spawn(select(2, ...))
+end, ...)
+";
+
 async fn wait(_: &Lua, secs: Option<f64>) -> LuaResult<f64> {
     let duration = Duration::from_secs_f64(secs.unwrap_or_default());
 
     let before = Instant::now();
-    time::sleep(duration).await;
+    sleep(duration).await;
     let after = Instant::now();
 
     Ok((after - before).as_secs_f64())
diff --git a/crates/lune-std/Cargo.toml b/crates/lune-std/Cargo.toml
new file mode 100644
index 0000000..3ee4c9a
--- /dev/null
+++ b/crates/lune-std/Cargo.toml
@@ -0,0 +1,57 @@
+[package]
+name = "lune-std"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[features]
+default = [
+    "datetime",
+    "fs",
+    "luau",
+    "net",
+    "process",
+    "regex",
+    "roblox",
+    "serde",
+    "stdio",
+    "task",
+]
+
+datetime = ["dep:lune-std-datetime"]
+fs = ["dep:lune-std-fs"]
+luau = ["dep:lune-std-luau"]
+net = ["dep:lune-std-net"]
+process = ["dep:lune-std-process"]
+regex = ["dep:lune-std-regex"]
+roblox = ["dep:lune-std-roblox"]
+serde = ["dep:lune-std-serde"]
+stdio = ["dep:lune-std-stdio"]
+task = ["dep:lune-std-task"]
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+mlua-luau-scheduler = "0.0.2"
+
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+tokio = { version = "1", default-features = false, features = ["fs", "sync"] }
+
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
+
+lune-std-datetime = { optional = true, version = "0.1.0", path = "../lune-std-datetime" }
+lune-std-fs = { optional = true, version = "0.1.0", path = "../lune-std-fs" }
+lune-std-luau = { optional = true, version = "0.1.0", path = "../lune-std-luau" }
+lune-std-net = { optional = true, version = "0.1.0", path = "../lune-std-net" }
+lune-std-process = { optional = true, version = "0.1.0", path = "../lune-std-process" }
+lune-std-regex = { optional = true, version = "0.1.0", path = "../lune-std-regex" }
+lune-std-roblox = { optional = true, version = "0.1.0", path = "../lune-std-roblox" }
+lune-std-serde = { optional = true, version = "0.1.0", path = "../lune-std-serde" }
+lune-std-stdio = { optional = true, version = "0.1.0", path = "../lune-std-stdio" }
+lune-std-task = { optional = true, version = "0.1.0", path = "../lune-std-task" }
diff --git a/crates/lune-std/src/global.rs b/crates/lune-std/src/global.rs
new file mode 100644
index 0000000..1c0944f
--- /dev/null
+++ b/crates/lune-std/src/global.rs
@@ -0,0 +1,92 @@
+use std::str::FromStr;
+
+use mlua::prelude::*;
+
+/**
+    A standard global provided by Lune.
+*/
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+pub enum LuneStandardGlobal {
+    GTable,
+    Print,
+    Require,
+    Version,
+    Warn,
+}
+
+impl LuneStandardGlobal {
+    /**
+        All available standard globals.
+    */
+    pub const ALL: &'static [Self] = &[
+        Self::GTable,
+        Self::Print,
+        Self::Require,
+        Self::Version,
+        Self::Warn,
+    ];
+
+    /**
+        Gets the name of the global, such as `_G` or `require`.
+    */
+    #[must_use]
+    pub fn name(&self) -> &'static str {
+        match self {
+            Self::GTable => "_G",
+            Self::Print => "print",
+            Self::Require => "require",
+            Self::Version => "_VERSION",
+            Self::Warn => "warn",
+        }
+    }
+
+    /**
+        Creates the Lua value for the global.
+
+        # Errors
+
+        If the global could not be created.
+    */
+    #[rustfmt::skip]
+    #[allow(unreachable_patterns)]
+    pub fn create<'lua>(&self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
+        let res = match self {
+            Self::GTable => crate::globals::g_table::create(lua),
+            Self::Print => crate::globals::print::create(lua),
+            Self::Require => crate::globals::require::create(lua),
+            Self::Version => crate::globals::version::create(lua),
+            Self::Warn => crate::globals::warn::create(lua),
+        };
+        match res {
+            Ok(v) => Ok(v),
+            Err(e) => Err(e.context(format!(
+                "Failed to create standard global '{}'",
+                self.name()
+            ))),
+        }
+    }
+}
+
+impl FromStr for LuneStandardGlobal {
+    type Err = String;
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let low = s.trim().to_ascii_lowercase();
+        Ok(match low.as_str() {
+            "_g" => Self::GTable,
+            "print" => Self::Print,
+            "require" => Self::Require,
+            "_version" => Self::Version,
+            "warn" => Self::Warn,
+            _ => {
+                return Err(format!(
+                    "Unknown standard global '{low}'\nValid globals are: {}",
+                    Self::ALL
+                        .iter()
+                        .map(Self::name)
+                        .collect::<Vec<_>>()
+                        .join(", ")
+                ))
+            }
+        })
+    }
+}
diff --git a/crates/lune-std/src/globals/g_table.rs b/crates/lune-std/src/globals/g_table.rs
new file mode 100644
index 0000000..e21fa1e
--- /dev/null
+++ b/crates/lune-std/src/globals/g_table.rs
@@ -0,0 +1,5 @@
+use mlua::prelude::*;
+
+pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
+    lua.create_table()?.into_lua(lua)
+}
diff --git a/crates/lune-std/src/globals/mod.rs b/crates/lune-std/src/globals/mod.rs
new file mode 100644
index 0000000..b60e9a1
--- /dev/null
+++ b/crates/lune-std/src/globals/mod.rs
@@ -0,0 +1,5 @@
+pub mod g_table;
+pub mod print;
+pub mod require;
+pub mod version;
+pub mod warn;
diff --git a/crates/lune-std/src/globals/print.rs b/crates/lune-std/src/globals/print.rs
new file mode 100644
index 0000000..3b8d5f1
--- /dev/null
+++ b/crates/lune-std/src/globals/print.rs
@@ -0,0 +1,19 @@
+use std::io::Write;
+
+use lune_utils::fmt::{pretty_format_multi_value, ValueFormatConfig};
+use mlua::prelude::*;
+
+const FORMAT_CONFIG: ValueFormatConfig = ValueFormatConfig::new()
+    .with_max_depth(4)
+    .with_colors_enabled(true);
+
+pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
+    let f = lua.create_function(|_, args: LuaMultiValue| {
+        let formatted = format!("{}\n", pretty_format_multi_value(&args, &FORMAT_CONFIG));
+        let mut stdout = std::io::stdout();
+        stdout.write_all(formatted.as_bytes())?;
+        stdout.flush()?;
+        Ok(())
+    })?;
+    f.into_lua(lua)
+}
diff --git a/src/lune/globals/require/alias.rs b/crates/lune-std/src/globals/require/alias.rs
similarity index 88%
rename from src/lune/globals/require/alias.rs
rename to crates/lune-std/src/globals/require/alias.rs
index 09a36b0..924056b 100644
--- a/src/lune/globals/require/alias.rs
+++ b/crates/lune-std/src/globals/require/alias.rs
@@ -1,10 +1,8 @@
-use console::style;
 use mlua::prelude::*;
 
-use crate::lune::util::{
-    luaurc::LuauRc,
-    paths::{make_absolute_and_clean, CWD},
-};
+use lune_utils::path::{clean_path_and_make_absolute, diff_path, get_current_dir};
+
+use crate::luaurc::LuauRc;
 
 use super::context::*;
 
@@ -20,7 +18,7 @@ where
 {
     let alias = alias.to_ascii_lowercase();
 
-    let parent = make_absolute_and_clean(source)
+    let parent = clean_path_and_make_absolute(source)
         .parent()
         .expect("how did a root path end up here..")
         .to_path_buf();
@@ -55,7 +53,7 @@ where
                     luaurc
                         .aliases()
                         .iter()
-                        .map(|(name, path)| format!("    {name} {} {path}", style(">").dim()))
+                        .map(|(name, path)| format!("    {name} > {path}"))
                         .collect::<Vec<_>>()
                         .join("\n")
                 ))
@@ -67,7 +65,7 @@ where
     // We now have our aliased path, our path require function just needs it
     // in a slightly different format with both absolute + relative to cwd
     let abs_path = luaurc.find_alias(&alias).unwrap().join(path);
-    let rel_path = pathdiff::diff_paths(&abs_path, CWD.as_path()).ok_or_else(|| {
+    let rel_path = diff_path(&abs_path, get_current_dir()).ok_or_else(|| {
         LuaError::runtime(format!("failed to find relative path for alias '{alias}'"))
     })?;
 
diff --git a/src/lune/globals/require/context.rs b/crates/lune-std/src/globals/require/context.rs
similarity index 86%
rename from src/lune/globals/require/context.rs
rename to crates/lune-std/src/globals/require/context.rs
index 7f018c6..0355d27 100644
--- a/src/lune/globals/require/context.rs
+++ b/crates/lune-std/src/globals/require/context.rs
@@ -6,15 +6,18 @@ use std::{
 
 use mlua::prelude::*;
 use mlua_luau_scheduler::LuaSchedulerExt;
+
 use tokio::{
-    fs,
+    fs::read,
     sync::{
         broadcast::{self, Sender},
         Mutex as AsyncMutex,
     },
 };
 
-use crate::lune::{builtins::LuneBuiltin, util::paths::CWD};
+use lune_utils::path::{clean_path, clean_path_and_make_absolute};
+
+use crate::library::LuneStandardLibrary;
 
 /**
     Context containing cached results for all `require` operations.
@@ -24,9 +27,9 @@ use crate::lune::{builtins::LuneBuiltin, util::paths::CWD};
 */
 #[derive(Debug, Clone)]
 pub(super) struct RequireContext {
-    cache_builtins: Arc<AsyncMutex<HashMap<LuneBuiltin, LuaResult<LuaRegistryKey>>>>,
-    cache_results: Arc<AsyncMutex<HashMap<PathBuf, LuaResult<LuaRegistryKey>>>>,
-    cache_pending: Arc<AsyncMutex<HashMap<PathBuf, Sender<()>>>>,
+    libraries: Arc<AsyncMutex<HashMap<LuneStandardLibrary, LuaResult<LuaRegistryKey>>>>,
+    results: Arc<AsyncMutex<HashMap<PathBuf, LuaResult<LuaRegistryKey>>>>,
+    pending: Arc<AsyncMutex<HashMap<PathBuf, Sender<()>>>>,
 }
 
 impl RequireContext {
@@ -39,9 +42,9 @@ impl RequireContext {
     */
     pub fn new() -> Self {
         Self {
-            cache_builtins: Arc::new(AsyncMutex::new(HashMap::new())),
-            cache_results: Arc::new(AsyncMutex::new(HashMap::new())),
-            cache_pending: Arc::new(AsyncMutex::new(HashMap::new())),
+            libraries: Arc::new(AsyncMutex::new(HashMap::new())),
+            results: Arc::new(AsyncMutex::new(HashMap::new())),
+            pending: Arc::new(AsyncMutex::new(HashMap::new())),
         }
     }
 
@@ -54,7 +57,6 @@ impl RequireContext {
         absolute path by prepending the current working directory.
     */
     pub fn resolve_paths(
-        &self,
         source: impl AsRef<str>,
         path: impl AsRef<str>,
     ) -> LuaResult<(PathBuf, PathBuf)> {
@@ -63,12 +65,8 @@ impl RequireContext {
             .ok_or_else(|| LuaError::runtime("Failed to get parent path of source"))?
             .join(path.as_ref());
 
-        let rel_path = path_clean::clean(path);
-        let abs_path = if rel_path.is_absolute() {
-            rel_path.to_path_buf()
-        } else {
-            CWD.join(&rel_path)
-        };
+        let abs_path = clean_path_and_make_absolute(&path);
+        let rel_path = clean_path(path);
 
         Ok((abs_path, rel_path))
     }
@@ -78,7 +76,7 @@ impl RequireContext {
     */
     pub fn is_cached(&self, abs_path: impl AsRef<Path>) -> LuaResult<bool> {
         let is_cached = self
-            .cache_results
+            .results
             .try_lock()
             .expect("RequireContext may not be used from multiple threads")
             .contains_key(abs_path.as_ref());
@@ -90,7 +88,7 @@ impl RequireContext {
     */
     pub fn is_pending(&self, abs_path: impl AsRef<Path>) -> LuaResult<bool> {
         let is_pending = self
-            .cache_pending
+            .pending
             .try_lock()
             .expect("RequireContext may not be used from multiple threads")
             .contains_key(abs_path.as_ref());
@@ -108,7 +106,7 @@ impl RequireContext {
         abs_path: impl AsRef<Path>,
     ) -> LuaResult<LuaMultiValue<'lua>> {
         let results = self
-            .cache_results
+            .results
             .try_lock()
             .expect("RequireContext may not be used from multiple threads");
 
@@ -138,7 +136,7 @@ impl RequireContext {
     ) -> LuaResult<LuaMultiValue<'lua>> {
         let mut thread_recv = {
             let pending = self
-                .cache_pending
+                .pending
                 .try_lock()
                 .expect("RequireContext may not be used from multiple threads");
             let thread_id = pending
@@ -163,7 +161,7 @@ impl RequireContext {
 
         // Read the file at the given path, try to parse and
         // load it into a new lua thread that we can schedule
-        let file_contents = fs::read(&abs_path).await?;
+        let file_contents = read(&abs_path).await?;
         let file_thread = lua
             .load(file_contents)
             .set_name(rel_path.to_string_lossy().to_string());
@@ -201,7 +199,7 @@ impl RequireContext {
 
         // Set this abs path as currently pending
         let (broadcast_tx, _) = broadcast::channel(1);
-        self.cache_pending
+        self.pending
             .try_lock()
             .expect("RequireContext may not be used from multiple threads")
             .insert(abs_path.to_path_buf(), broadcast_tx);
@@ -221,7 +219,7 @@ impl RequireContext {
         // NOTE: We use the async lock and not try_lock here because
         // some other thread may be wanting to insert into the require
         // cache at the same time, and that's not an actual error case
-        self.cache_results
+        self.results
             .lock()
             .await
             .insert(abs_path.to_path_buf(), load_res);
@@ -230,7 +228,7 @@ impl RequireContext {
         // broadcast a message to let any listeners know that this
         // path has now finished the require process and is cached
         let broadcast_tx = self
-            .cache_pending
+            .pending
             .try_lock()
             .expect("RequireContext may not be used from multiple threads")
             .remove(abs_path)
@@ -241,39 +239,39 @@ impl RequireContext {
     }
 
     /**
-        Loads (requires) the builtin with the given name.
+        Loads (requires) the library with the given name.
     */
-    pub fn load_builtin<'lua>(
+    pub fn load_library<'lua>(
         &self,
         lua: &'lua Lua,
         name: impl AsRef<str>,
     ) -> LuaResult<LuaMultiValue<'lua>> {
-        let builtin: LuneBuiltin = match name.as_ref().parse() {
+        let library: LuneStandardLibrary = match name.as_ref().parse() {
             Err(e) => return Err(LuaError::runtime(e)),
             Ok(b) => b,
         };
 
         let mut cache = self
-            .cache_builtins
+            .libraries
             .try_lock()
             .expect("RequireContext may not be used from multiple threads");
 
-        if let Some(res) = cache.get(&builtin) {
+        if let Some(res) = cache.get(&library) {
             return match res {
                 Err(e) => return Err(e.clone()),
                 Ok(key) => {
                     let multi_vec = lua
                         .registry_value::<Vec<LuaValue>>(key)
-                        .expect("Missing builtin result in lua registry");
+                        .expect("Missing library result in lua registry");
                     Ok(LuaMultiValue::from_vec(multi_vec))
                 }
             };
         };
 
-        let result = builtin.create(lua);
+        let result = library.module(lua);
 
         cache.insert(
-            builtin,
+            library,
             match result.clone() {
                 Err(e) => Err(e),
                 Ok(multi) => {
diff --git a/src/lune/globals/require/builtin.rs b/crates/lune-std/src/globals/require/library.rs
similarity index 70%
rename from src/lune/globals/require/builtin.rs
rename to crates/lune-std/src/globals/require/library.rs
index 42302cf..b47ea92 100644
--- a/src/lune/globals/require/builtin.rs
+++ b/crates/lune-std/src/globals/require/library.rs
@@ -2,7 +2,7 @@ use mlua::prelude::*;
 
 use super::context::*;
 
-pub(super) async fn require<'lua, 'ctx>(
+pub(super) fn require<'lua, 'ctx>(
     lua: &'lua Lua,
     ctx: &'ctx RequireContext,
     name: &str,
@@ -10,5 +10,5 @@ pub(super) async fn require<'lua, 'ctx>(
 where
     'lua: 'ctx,
 {
-    ctx.load_builtin(lua, name)
+    ctx.load_library(lua, name)
 }
diff --git a/src/lune/globals/require/mod.rs b/crates/lune-std/src/globals/require/mod.rs
similarity index 85%
rename from src/lune/globals/require/mod.rs
rename to crates/lune-std/src/globals/require/mod.rs
index 1a83e58..3876e36 100644
--- a/src/lune/globals/require/mod.rs
+++ b/crates/lune-std/src/globals/require/mod.rs
@@ -1,19 +1,19 @@
 use mlua::prelude::*;
 
-use crate::lune::util::TableBuilder;
+use lune_utils::TableBuilder;
 
 mod context;
 use context::RequireContext;
 
 mod alias;
-mod builtin;
+mod library;
 mod path;
 
-const REQUIRE_IMPL: &str = r#"
+const REQUIRE_IMPL: &str = r"
 return require(source(), ...)
-"#;
+";
 
-pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
+pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
     lua.set_app_data(RequireContext::new());
 
     /*
@@ -36,7 +36,7 @@ pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
     */
 
     let require_fn = lua.create_async_function(require)?;
-    let get_source_fn = lua.create_function(move |lua, _: ()| match lua.inspect_stack(2) {
+    let get_source_fn = lua.create_function(move |lua, (): ()| match lua.inspect_stack(2) {
         None => Err(LuaError::runtime(
             "Failed to get stack info for require source",
         )),
@@ -56,7 +56,8 @@ pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
     lua.load(REQUIRE_IMPL)
         .set_name("require")
         .set_environment(require_env)
-        .into_function()
+        .into_function()?
+        .into_lua(lua)
 }
 
 async fn require<'lua>(
@@ -79,11 +80,8 @@ async fn require<'lua>(
         .app_data_ref()
         .expect("Failed to get RequireContext from app data");
 
-    if let Some(builtin_name) = path
-        .strip_prefix("@lune/")
-        .map(|name| name.to_ascii_lowercase())
-    {
-        builtin::require(lua, &context, &builtin_name).await
+    if let Some(builtin_name) = path.strip_prefix("@lune/").map(str::to_ascii_lowercase) {
+        library::require(lua, &context, &builtin_name)
     } else if let Some(aliased_path) = path.strip_prefix('@') {
         let (alias, path) = aliased_path.split_once('/').ok_or(LuaError::runtime(
             "Require with custom alias must contain '/' delimiter",
diff --git a/src/lune/globals/require/path.rs b/crates/lune-std/src/globals/require/path.rs
similarity index 97%
rename from src/lune/globals/require/path.rs
rename to crates/lune-std/src/globals/require/path.rs
index 7e8084f..1fabebf 100644
--- a/src/lune/globals/require/path.rs
+++ b/crates/lune-std/src/globals/require/path.rs
@@ -14,7 +14,7 @@ pub(super) async fn require<'lua, 'ctx>(
 where
     'lua: 'ctx,
 {
-    let (abs_path, rel_path) = ctx.resolve_paths(source, path)?;
+    let (abs_path, rel_path) = RequireContext::resolve_paths(source, path)?;
     require_abs_rel(lua, ctx, abs_path, rel_path).await
 }
 
diff --git a/crates/lune-std/src/globals/version.rs b/crates/lune-std/src/globals/version.rs
new file mode 100644
index 0000000..3eface4
--- /dev/null
+++ b/crates/lune-std/src/globals/version.rs
@@ -0,0 +1,35 @@
+use mlua::prelude::*;
+
+use lune_utils::get_version_string;
+
+struct Version(String);
+
+impl LuaUserData for Version {}
+
+pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
+    let v = match lua.app_data_ref::<Version>() {
+        Some(v) => v.0.to_string(),
+        None => env!("CARGO_PKG_VERSION").to_string(),
+    };
+    let s = get_version_string(v);
+    lua.create_string(s)?.into_lua(lua)
+}
+
+/**
+    Overrides the version string to be used by the `_VERSION` global.
+
+    The global will be a string in the format `Lune x.y.z+luau`,
+    where `x.y.z` is the string passed to this function.
+
+    The version string passed should be the version of the Lune runtime,
+    obtained from `env!("CARGO_PKG_VERSION")` or a similar mechanism.
+
+    # Panics
+
+    Panics if the version string is empty or contains invalid characters.
+*/
+pub fn set_global_version(lua: &Lua, version: impl Into<String>) {
+    let v = version.into();
+    let _ = get_version_string(&v); // Validate version string
+    lua.set_app_data(Version(v));
+}
diff --git a/crates/lune-std/src/globals/warn.rs b/crates/lune-std/src/globals/warn.rs
new file mode 100644
index 0000000..ee42ddb
--- /dev/null
+++ b/crates/lune-std/src/globals/warn.rs
@@ -0,0 +1,23 @@
+use std::io::Write;
+
+use lune_utils::fmt::{pretty_format_multi_value, Label, ValueFormatConfig};
+use mlua::prelude::*;
+
+const FORMAT_CONFIG: ValueFormatConfig = ValueFormatConfig::new()
+    .with_max_depth(4)
+    .with_colors_enabled(true);
+
+pub fn create(lua: &Lua) -> LuaResult<LuaValue> {
+    let f = lua.create_function(|_, args: LuaMultiValue| {
+        let formatted = format!(
+            "{}\n{}\n",
+            Label::Warn,
+            pretty_format_multi_value(&args, &FORMAT_CONFIG)
+        );
+        let mut stdout = std::io::stdout();
+        stdout.write_all(formatted.as_bytes())?;
+        stdout.flush()?;
+        Ok(())
+    })?;
+    f.into_lua(lua)
+}
diff --git a/crates/lune-std/src/lib.rs b/crates/lune-std/src/lib.rs
new file mode 100644
index 0000000..a29bef0
--- /dev/null
+++ b/crates/lune-std/src/lib.rs
@@ -0,0 +1,29 @@
+#![allow(clippy::cargo_common_metadata)]
+
+use mlua::prelude::*;
+
+mod global;
+mod globals;
+mod library;
+mod luaurc;
+
+pub use self::global::LuneStandardGlobal;
+pub use self::globals::version::set_global_version;
+pub use self::library::LuneStandardLibrary;
+
+/**
+    Injects all standard globals into the given Lua state / VM.
+
+    This includes all enabled standard libraries, which can
+    be used from Lua with `require("@lune/library-name")`.
+
+    # Errors
+
+    Errors when out of memory, or if *default* Lua globals are missing.
+*/
+pub fn inject_globals(lua: &Lua) -> LuaResult<()> {
+    for global in LuneStandardGlobal::ALL {
+        lua.globals().set(global.name(), global.create(lua)?)?;
+    }
+    Ok(())
+}
diff --git a/crates/lune-std/src/library.rs b/crates/lune-std/src/library.rs
new file mode 100644
index 0000000..9a301f5
--- /dev/null
+++ b/crates/lune-std/src/library.rs
@@ -0,0 +1,127 @@
+use std::str::FromStr;
+
+use mlua::prelude::*;
+
+/**
+    A standard library provided by Lune.
+*/
+#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+#[rustfmt::skip]
+pub enum LuneStandardLibrary {
+    #[cfg(feature = "datetime")] DateTime,
+    #[cfg(feature = "fs")]       Fs,
+    #[cfg(feature = "luau")]     Luau,
+    #[cfg(feature = "net")]      Net,
+    #[cfg(feature = "task")]     Task,
+    #[cfg(feature = "process")]  Process,
+    #[cfg(feature = "regex")]    Regex,
+    #[cfg(feature = "serde")]    Serde,
+    #[cfg(feature = "stdio")]    Stdio,
+    #[cfg(feature = "roblox")]   Roblox,
+}
+
+impl LuneStandardLibrary {
+    /**
+        All available standard libraries.
+    */
+    #[rustfmt::skip]
+    pub const ALL: &'static [Self] = &[
+        #[cfg(feature = "datetime")] Self::DateTime,
+        #[cfg(feature = "fs")]       Self::Fs,
+        #[cfg(feature = "luau")]     Self::Luau,
+        #[cfg(feature = "net")]      Self::Net,
+        #[cfg(feature = "task")]     Self::Task,
+        #[cfg(feature = "process")]  Self::Process,
+        #[cfg(feature = "regex")]    Self::Regex,
+        #[cfg(feature = "serde")]    Self::Serde,
+        #[cfg(feature = "stdio")]    Self::Stdio,
+        #[cfg(feature = "roblox")]   Self::Roblox,
+    ];
+
+    /**
+        Gets the name of the library, such as `datetime` or `fs`.
+    */
+    #[must_use]
+    #[rustfmt::skip]
+    #[allow(unreachable_patterns)]
+    pub fn name(&self) -> &'static str {
+        match self {
+            #[cfg(feature = "datetime")] Self::DateTime => "datetime",
+            #[cfg(feature = "fs")]       Self::Fs       => "fs",
+            #[cfg(feature = "luau")]     Self::Luau     => "luau",
+            #[cfg(feature = "net")]      Self::Net      => "net",
+            #[cfg(feature = "task")]     Self::Task     => "task",
+            #[cfg(feature = "process")]  Self::Process  => "process",
+            #[cfg(feature = "regex")]    Self::Regex    => "regex",
+            #[cfg(feature = "serde")]    Self::Serde    => "serde",
+            #[cfg(feature = "stdio")]    Self::Stdio    => "stdio",
+            #[cfg(feature = "roblox")]   Self::Roblox   => "roblox",
+
+            _ => unreachable!("no standard library enabled"),
+        }
+    }
+
+    /**
+        Creates the Lua module for the library.
+
+        # Errors
+
+        If the library could not be created.
+    */
+    #[rustfmt::skip]
+    #[allow(unreachable_patterns)]
+    pub fn module<'lua>(&self, lua: &'lua Lua) -> LuaResult<LuaMultiValue<'lua>> {
+        let res: LuaResult<LuaTable> = match self {
+            #[cfg(feature = "datetime")] Self::DateTime => lune_std_datetime::module(lua),
+            #[cfg(feature = "fs")]       Self::Fs       => lune_std_fs::module(lua),
+            #[cfg(feature = "luau")]     Self::Luau     => lune_std_luau::module(lua),
+            #[cfg(feature = "net")]      Self::Net      => lune_std_net::module(lua),
+            #[cfg(feature = "task")]     Self::Task     => lune_std_task::module(lua),
+            #[cfg(feature = "process")]  Self::Process  => lune_std_process::module(lua),
+            #[cfg(feature = "regex")]    Self::Regex    => lune_std_regex::module(lua),
+            #[cfg(feature = "serde")]    Self::Serde    => lune_std_serde::module(lua),
+            #[cfg(feature = "stdio")]    Self::Stdio    => lune_std_stdio::module(lua),
+            #[cfg(feature = "roblox")]   Self::Roblox   => lune_std_roblox::module(lua),
+
+            _ => unreachable!("no standard library enabled"),
+        };
+        match res {
+            Ok(v) => v.into_lua_multi(lua),
+            Err(e) => Err(e.context(format!(
+                "Failed to create standard library '{}'",
+                self.name()
+            ))),
+        }
+    }
+}
+
+impl FromStr for LuneStandardLibrary {
+    type Err = String;
+    #[rustfmt::skip]
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let low = s.trim().to_ascii_lowercase();
+        Ok(match low.as_str() {
+            #[cfg(feature = "datetime")] "datetime" => Self::DateTime,
+            #[cfg(feature = "fs")]       "fs"       => Self::Fs,
+            #[cfg(feature = "luau")]     "luau"     => Self::Luau,
+            #[cfg(feature = "net")]      "net"      => Self::Net,
+            #[cfg(feature = "task")]     "task"     => Self::Task,
+            #[cfg(feature = "process")]  "process"  => Self::Process,
+            #[cfg(feature = "regex")]    "regex"    => Self::Regex,
+            #[cfg(feature = "serde")]    "serde"    => Self::Serde,
+            #[cfg(feature = "stdio")]    "stdio"    => Self::Stdio,
+            #[cfg(feature = "roblox")]   "roblox"   => Self::Roblox,
+
+            _ => {
+                return Err(format!(
+                    "Unknown standard library '{low}'\nValid libraries are: {}",
+                    Self::ALL
+                        .iter()
+                        .map(Self::name)
+                        .collect::<Vec<_>>()
+                        .join(", ")
+                ))
+            }
+        })
+    }
+}
diff --git a/src/lune/util/luaurc.rs b/crates/lune-std/src/luaurc.rs
similarity index 65%
rename from src/lune/util/luaurc.rs
rename to crates/lune-std/src/luaurc.rs
index 69ac64a..0eada59 100644
--- a/src/lune/util/luaurc.rs
+++ b/crates/lune-std/src/luaurc.rs
@@ -1,20 +1,20 @@
 use std::{
     collections::HashMap,
     path::{Path, PathBuf, MAIN_SEPARATOR},
+    sync::Arc,
 };
 
-use path_clean::PathClean;
 use serde::{Deserialize, Serialize};
 use serde_json::Value as JsonValue;
-use tokio::fs;
+use tokio::fs::read;
 
-use super::paths::make_absolute_and_clean;
+use lune_utils::path::{clean_path, clean_path_and_make_absolute};
 
 const LUAURC_FILE: &str = ".luaurc";
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "lowercase")]
-pub enum LuauLanguageMode {
+enum LuauLanguageMode {
     NoCheck,
     NonStrict,
     Strict,
@@ -22,7 +22,7 @@ pub enum LuauLanguageMode {
 
 #[derive(Debug, Clone, Default, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
-pub struct LuauRcConfig {
+struct LuauRcConfig {
     #[serde(skip_serializing_if = "Option::is_none")]
     language_mode: Option<LuauLanguageMode>,
     #[serde(skip_serializing_if = "Option::is_none")]
@@ -39,26 +39,45 @@ pub struct LuauRcConfig {
     aliases: Option<HashMap<String, String>>,
 }
 
+/**
+    A deserialized `.luaurc` file.
+
+    Contains utility methods for validating and searching for aliases.
+*/
 #[derive(Debug, Clone)]
 pub struct LuauRc {
-    dir: PathBuf,
+    dir: Arc<Path>,
     config: LuauRcConfig,
 }
 
 impl LuauRc {
+    /**
+        Reads a `.luaurc` file from the given directory.
+
+        If the file does not exist, or if it is invalid, this function returns `None`.
+    */
     pub async fn read(dir: impl AsRef<Path>) -> Option<Self> {
-        let dir = make_absolute_and_clean(dir);
+        let dir = clean_path_and_make_absolute(dir);
         let path = dir.join(LUAURC_FILE);
-        let bytes = fs::read(&path).await.ok()?;
+        let bytes = read(&path).await.ok()?;
         let config = serde_json::from_slice(&bytes).ok()?;
-        Some(Self { dir, config })
+        Some(Self {
+            dir: dir.into(),
+            config,
+        })
     }
 
+    /**
+        Reads a `.luaurc` file from the given directory, and then recursively searches
+        for a `.luaurc` file in the parent directories if a predicate is not satisfied.
+
+        If no `.luaurc` file exists, or if they are invalid, this function returns `None`.
+    */
     pub async fn read_recursive(
         dir: impl AsRef<Path>,
         mut predicate: impl FnMut(&Self) -> bool,
     ) -> Option<Self> {
-        let mut current = make_absolute_and_clean(dir);
+        let mut current = clean_path_and_make_absolute(dir);
         loop {
             if let Some(rc) = Self::read(&current).await {
                 if predicate(&rc) {
@@ -73,21 +92,43 @@ impl LuauRc {
         }
     }
 
+    /**
+        Validates that the `.luaurc` file is correct.
+
+        This primarily validates aliases since they are not
+        validated during creation of the [`LuauRc`] struct.
+
+        # Errors
+
+        If an alias key is invalid.
+    */
     pub fn validate(&self) -> Result<(), String> {
         if let Some(aliases) = &self.config.aliases {
             for alias in aliases.keys() {
                 if !is_valid_alias_key(alias) {
-                    return Err(format!("invalid alias key: {}", alias));
+                    return Err(format!("invalid alias key: {alias}"));
                 }
             }
         }
         Ok(())
     }
 
+    /**
+        Gets a copy of all aliases in the `.luaurc` file.
+
+        Will return an empty map if there are no aliases.
+    */
+    #[must_use]
     pub fn aliases(&self) -> HashMap<String, String> {
         self.config.aliases.clone().unwrap_or_default()
     }
 
+    /**
+        Finds an alias in the `.luaurc` file by name.
+
+        If the alias does not exist, this function returns `None`.
+    */
+    #[must_use]
     pub fn find_alias(&self, name: &str) -> Option<PathBuf> {
         self.config.aliases.as_ref().and_then(|aliases| {
             aliases.iter().find_map(|(alias, path)| {
@@ -96,7 +137,7 @@ impl LuauRc {
                     .eq_ignore_ascii_case(name)
                     && is_valid_alias_key(alias)
                 {
-                    Some(self.dir.join(path).clean())
+                    Some(clean_path(self.dir.join(path)))
                 } else {
                     None
                 }
diff --git a/crates/lune-utils/Cargo.toml b/crates/lune-utils/Cargo.toml
new file mode 100644
index 0000000..dd39731
--- /dev/null
+++ b/crates/lune-utils/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "lune-utils"
+version = "0.1.0"
+edition = "2021"
+license = "MPL-2.0"
+
+[lib]
+path = "src/lib.rs"
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau", "async"] }
+
+tokio = { version = "1", default-features = false, features = ["fs"] }
+
+console = "0.15"
+dunce = "1.0"
+once_cell = "1.17"
+path-clean = "1.0"
+pathdiff = "0.2"
diff --git a/crates/lune-utils/src/fmt/error/components.rs b/crates/lune-utils/src/fmt/error/components.rs
new file mode 100644
index 0000000..941b8d0
--- /dev/null
+++ b/crates/lune-utils/src/fmt/error/components.rs
@@ -0,0 +1,152 @@
+use std::fmt;
+use std::str::FromStr;
+use std::sync::Arc;
+
+use console::style;
+use mlua::prelude::*;
+use once_cell::sync::Lazy;
+
+use super::StackTrace;
+
+static STYLED_STACK_BEGIN: Lazy<String> = Lazy::new(|| {
+    format!(
+        "{}{}{}",
+        style("[").dim(),
+        style("Stack Begin").blue(),
+        style("]").dim()
+    )
+});
+
+static STYLED_STACK_END: Lazy<String> = Lazy::new(|| {
+    format!(
+        "{}{}{}",
+        style("[").dim(),
+        style("Stack End").blue(),
+        style("]").dim()
+    )
+});
+
+/**
+    Error components parsed from a [`LuaError`].
+
+    Can be used to display a human-friendly error message
+    and stack trace, in the following Roblox-inspired format:
+
+    ```plaintext
+    Error message
+    [Stack Begin]
+        Stack trace line
+        Stack trace line
+        Stack trace line
+    [Stack End]
+    ```
+*/
+#[derive(Debug, Default, Clone)]
+pub struct ErrorComponents {
+    messages: Vec<String>,
+    trace: Option<StackTrace>,
+}
+
+impl ErrorComponents {
+    /**
+        Returns the error messages.
+    */
+    #[must_use]
+    pub fn messages(&self) -> &[String] {
+        &self.messages
+    }
+
+    /**
+        Returns the stack trace, if it exists.
+    */
+    #[must_use]
+    pub fn trace(&self) -> Option<&StackTrace> {
+        self.trace.as_ref()
+    }
+
+    /**
+        Returns `true` if the error has a non-empty stack trace.
+
+        Note that a trace may still *exist*, but it may be empty.
+    */
+    #[must_use]
+    pub fn has_trace(&self) -> bool {
+        self.trace
+            .as_ref()
+            .is_some_and(|trace| !trace.lines().is_empty())
+    }
+}
+
+impl fmt::Display for ErrorComponents {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        for message in self.messages() {
+            writeln!(f, "{message}")?;
+        }
+        if self.has_trace() {
+            let trace = self.trace.as_ref().unwrap();
+            writeln!(f, "{}", *STYLED_STACK_BEGIN)?;
+            for line in trace.lines() {
+                writeln!(f, "\t{line}")?;
+            }
+            writeln!(f, "{}", *STYLED_STACK_END)?;
+        }
+        Ok(())
+    }
+}
+
+impl From<LuaError> for ErrorComponents {
+    fn from(error: LuaError) -> Self {
+        fn lua_error_message(e: &LuaError) -> String {
+            if let LuaError::RuntimeError(s) = e {
+                s.to_string()
+            } else {
+                e.to_string()
+            }
+        }
+
+        fn lua_stack_trace(source: &str) -> Option<StackTrace> {
+            // FUTURE: Preserve a parsing error here somehow?
+            // Maybe we can emit parsing errors using tracing?
+            StackTrace::from_str(source).ok()
+        }
+
+        // Extract any additional "context" messages before the actual error(s)
+        // The Arc is necessary here because mlua wraps all inner errors in an Arc
+        let mut error = Arc::new(error);
+        let mut messages = Vec::new();
+        while let LuaError::WithContext {
+            ref context,
+            ref cause,
+        } = *error
+        {
+            messages.push(context.to_string());
+            error = cause.clone();
+        }
+
+        // We will then try to extract any stack trace
+        let trace = if let LuaError::CallbackError {
+            ref traceback,
+            ref cause,
+        } = *error
+        {
+            messages.push(lua_error_message(cause));
+            lua_stack_trace(traceback)
+        } else if let LuaError::RuntimeError(ref s) = *error {
+            // NOTE: Runtime errors may include tracebacks, but they're
+            // joined with error messages, so we need to split them out
+            if let Some(pos) = s.find("stack traceback:") {
+                let (message, traceback) = s.split_at(pos);
+                messages.push(message.trim().to_string());
+                lua_stack_trace(traceback)
+            } else {
+                messages.push(s.to_string());
+                None
+            }
+        } else {
+            messages.push(lua_error_message(&error));
+            None
+        };
+
+        ErrorComponents { messages, trace }
+    }
+}
diff --git a/crates/lune-utils/src/fmt/error/mod.rs b/crates/lune-utils/src/fmt/error/mod.rs
new file mode 100644
index 0000000..00d0658
--- /dev/null
+++ b/crates/lune-utils/src/fmt/error/mod.rs
@@ -0,0 +1,8 @@
+mod components;
+mod stack_trace;
+
+#[cfg(test)]
+mod tests;
+
+pub use self::components::ErrorComponents;
+pub use self::stack_trace::{StackTrace, StackTraceLine, StackTraceSource};
diff --git a/crates/lune-utils/src/fmt/error/stack_trace.rs b/crates/lune-utils/src/fmt/error/stack_trace.rs
new file mode 100644
index 0000000..a33ec9a
--- /dev/null
+++ b/crates/lune-utils/src/fmt/error/stack_trace.rs
@@ -0,0 +1,170 @@
+use std::fmt;
+use std::str::FromStr;
+
+fn parse_path(s: &str) -> Option<(&str, &str)> {
+    let path = s.strip_prefix("[string \"")?;
+    let (path, after) = path.split_once("\"]:")?;
+
+    // Remove line number after any found colon, this may
+    // exist if the source path is from a rust source file
+    let path = match path.split_once(':') {
+        Some((before, _)) => before,
+        None => path,
+    };
+
+    Some((path, after))
+}
+
+fn parse_function_name(s: &str) -> Option<&str> {
+    s.strip_prefix("in function '")
+        .and_then(|s| s.strip_suffix('\''))
+}
+
+fn parse_line_number(s: &str) -> (Option<usize>, &str) {
+    match s.split_once(':') {
+        Some((before, after)) => (before.parse::<usize>().ok(), after),
+        None => (None, s),
+    }
+}
+
+/**
+    Source of a stack trace line parsed from a [`LuaError`].
+*/
+#[derive(Debug, Default, Clone, Copy)]
+pub enum StackTraceSource {
+    /// Error originated from a C / Rust function.
+    C,
+    /// Error originated from a Lua (user) function.
+    #[default]
+    Lua,
+}
+
+/**
+    Stack trace line parsed from a [`LuaError`].
+*/
+#[derive(Debug, Default, Clone)]
+pub struct StackTraceLine {
+    source: StackTraceSource,
+    path: Option<String>,
+    line_number: Option<usize>,
+    function_name: Option<String>,
+}
+
+impl StackTraceLine {
+    /**
+        Returns the source of the stack trace line.
+    */
+    #[must_use]
+    pub fn source(&self) -> StackTraceSource {
+        self.source
+    }
+
+    /**
+        Returns the path, if it exists.
+    */
+    #[must_use]
+    pub fn path(&self) -> Option<&str> {
+        self.path.as_deref()
+    }
+
+    /**
+        Returns the line number, if it exists.
+    */
+    #[must_use]
+    pub fn line_number(&self) -> Option<usize> {
+        self.line_number
+    }
+
+    /**
+        Returns the function name, if it exists.
+    */
+    #[must_use]
+    pub fn function_name(&self) -> Option<&str> {
+        self.function_name.as_deref()
+    }
+}
+
+impl FromStr for StackTraceLine {
+    type Err = String;
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        if let Some(after) = s.strip_prefix("[C]: ") {
+            let function_name = parse_function_name(after).map(ToString::to_string);
+
+            Ok(Self {
+                source: StackTraceSource::C,
+                path: None,
+                line_number: None,
+                function_name,
+            })
+        } else if let Some((path, after)) = parse_path(s) {
+            let (line_number, after) = parse_line_number(after);
+            let function_name = parse_function_name(after).map(ToString::to_string);
+
+            Ok(Self {
+                source: StackTraceSource::Lua,
+                path: Some(path.to_string()),
+                line_number,
+                function_name,
+            })
+        } else {
+            Err(String::from("unknown format"))
+        }
+    }
+}
+
+impl fmt::Display for StackTraceLine {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if matches!(self.source, StackTraceSource::C) {
+            write!(f, "Script '[C]'")?;
+        } else {
+            write!(f, "Script '{}'", self.path.as_deref().unwrap_or("[?]"))?;
+            if let Some(line_number) = self.line_number {
+                write!(f, ", Line {line_number}")?;
+            }
+        }
+        if let Some(function_name) = self.function_name.as_deref() {
+            write!(f, " - function '{function_name}'")?;
+        }
+        Ok(())
+    }
+}
+
+/**
+    Stack trace parsed from a [`LuaError`].
+*/
+#[derive(Debug, Default, Clone)]
+pub struct StackTrace {
+    lines: Vec<StackTraceLine>,
+}
+
+impl StackTrace {
+    /**
+        Returns the individual stack trace lines.
+    */
+    #[must_use]
+    pub fn lines(&self) -> &[StackTraceLine] {
+        &self.lines
+    }
+}
+
+impl FromStr for StackTrace {
+    type Err = String;
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let (_, after) = s
+            .split_once("stack traceback:")
+            .ok_or_else(|| String::from("missing 'stack traceback:' prefix"))?;
+        let lines = after
+            .trim()
+            .lines()
+            .filter_map(|line| {
+                let line = line.trim();
+                if line.is_empty() {
+                    None
+                } else {
+                    Some(line.parse())
+                }
+            })
+            .collect::<Result<Vec<_>, _>>()?;
+        Ok(StackTrace { lines })
+    }
+}
diff --git a/crates/lune-utils/src/fmt/error/tests.rs b/crates/lune-utils/src/fmt/error/tests.rs
new file mode 100644
index 0000000..ff5a5f3
--- /dev/null
+++ b/crates/lune-utils/src/fmt/error/tests.rs
@@ -0,0 +1,85 @@
+use mlua::prelude::*;
+
+use crate::fmt::ErrorComponents;
+
+fn new_lua_result() -> LuaResult<()> {
+    let lua = Lua::new();
+
+    lua.globals()
+        .set(
+            "f",
+            LuaFunction::wrap(|_, (): ()| {
+                Err::<(), _>(LuaError::runtime("oh no, a runtime error"))
+            }),
+        )
+        .unwrap();
+
+    lua.load("f()").set_name("chunk_name").eval()
+}
+
+// Tests for error context stack
+mod context {
+    use super::*;
+
+    #[test]
+    fn preserves_original() {
+        let lua_error = new_lua_result().context("additional context").unwrap_err();
+        let components = ErrorComponents::from(lua_error);
+
+        assert_eq!(components.messages()[0], "additional context");
+        assert_eq!(components.messages()[1], "oh no, a runtime error");
+    }
+
+    #[test]
+    fn preserves_levels() {
+        // NOTE: The behavior in mlua is to preserve a single level of context
+        // and not all levels (context gets replaced on each call to `context`)
+        let lua_error = new_lua_result()
+            .context("level 1")
+            .context("level 2")
+            .context("level 3")
+            .unwrap_err();
+        let components = ErrorComponents::from(lua_error);
+
+        assert_eq!(
+            components.messages(),
+            &["level 3", "oh no, a runtime error"]
+        );
+    }
+}
+
+// Tests for error components struct: separated messages + stack trace
+mod error_components {
+    use super::*;
+
+    #[test]
+    fn message() {
+        let lua_error = new_lua_result().unwrap_err();
+        let components = ErrorComponents::from(lua_error);
+
+        assert_eq!(components.messages()[0], "oh no, a runtime error");
+    }
+
+    #[test]
+    fn stack_begin_end() {
+        let lua_error = new_lua_result().unwrap_err();
+        let formatted = format!("{}", ErrorComponents::from(lua_error));
+
+        assert!(formatted.contains("Stack Begin"));
+        assert!(formatted.contains("Stack End"));
+    }
+
+    #[test]
+    fn stack_lines() {
+        let lua_error = new_lua_result().unwrap_err();
+        let components = ErrorComponents::from(lua_error);
+
+        let mut lines = components.trace().unwrap().lines().iter();
+        let line_1 = lines.next().unwrap().to_string();
+        let line_2 = lines.next().unwrap().to_string();
+        assert!(lines.next().is_none());
+
+        assert_eq!(line_1, "Script '[C]' - function 'f'");
+        assert_eq!(line_2, "Script 'chunk_name', Line 1");
+    }
+}
diff --git a/crates/lune-utils/src/fmt/label.rs b/crates/lune-utils/src/fmt/label.rs
new file mode 100644
index 0000000..5e2e290
--- /dev/null
+++ b/crates/lune-utils/src/fmt/label.rs
@@ -0,0 +1,66 @@
+use std::fmt;
+
+use console::{style, Color};
+
+/**
+    Label enum used for consistent output formatting throughout Lune.
+
+    # Example usage
+
+    ```rs
+    use lune_utils::fmt::Label;
+
+    println!("{} This is an info message", Label::Info);
+    // [INFO] This is an info message
+
+    println!("{} This is a warning message", Label::Warn);
+    // [WARN] This is a warning message
+
+    println!("{} This is an error message", Label::Error);
+    // [ERROR] This is an error message
+    ```
+*/
+#[derive(Debug, Clone, Copy)]
+pub enum Label {
+    Info,
+    Warn,
+    Error,
+}
+
+impl Label {
+    /**
+        Returns the name of the label in all uppercase.
+    */
+    #[must_use]
+    pub fn name(&self) -> &str {
+        match self {
+            Self::Info => "INFO",
+            Self::Warn => "WARN",
+            Self::Error => "ERROR",
+        }
+    }
+
+    /**
+        Returns the color of the label.
+    */
+    #[must_use]
+    pub fn color(&self) -> Color {
+        match self {
+            Self::Info => Color::Blue,
+            Self::Warn => Color::Yellow,
+            Self::Error => Color::Red,
+        }
+    }
+}
+
+impl fmt::Display for Label {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "{}{}{}",
+            style("[").dim(),
+            style(self.name()).fg(self.color()),
+            style("]").dim()
+        )
+    }
+}
diff --git a/crates/lune-utils/src/fmt/mod.rs b/crates/lune-utils/src/fmt/mod.rs
new file mode 100644
index 0000000..0011feb
--- /dev/null
+++ b/crates/lune-utils/src/fmt/mod.rs
@@ -0,0 +1,7 @@
+mod error;
+mod label;
+mod value;
+
+pub use self::error::{ErrorComponents, StackTrace, StackTraceLine, StackTraceSource};
+pub use self::label::Label;
+pub use self::value::{pretty_format_multi_value, pretty_format_value, ValueFormatConfig};
diff --git a/crates/lune-utils/src/fmt/value/basic.rs b/crates/lune-utils/src/fmt/value/basic.rs
new file mode 100644
index 0000000..cc4f9fb
--- /dev/null
+++ b/crates/lune-utils/src/fmt/value/basic.rs
@@ -0,0 +1,74 @@
+use mlua::prelude::*;
+
+use super::{
+    metamethods::{call_table_tostring_metamethod, call_userdata_tostring_metamethod},
+    style::{COLOR_CYAN, COLOR_GREEN, COLOR_MAGENTA, COLOR_YELLOW},
+};
+
+const STRING_REPLACEMENTS: &[(&str, &str)] =
+    &[("\"", r#"\""#), ("\t", r"\t"), ("\r", r"\r"), ("\n", r"\n")];
+
+/**
+    Tries to return the given value as a plain string key.
+
+    A plain string key must:
+
+    - Start with an alphabetic character.
+    - Only contain alphanumeric characters and underscores.
+*/
+pub(crate) fn lua_value_as_plain_string_key(value: &LuaValue) -> Option<String> {
+    if let LuaValue::String(s) = value {
+        if let Ok(s) = s.to_str() {
+            let first_valid = s.chars().next().is_some_and(|c| c.is_ascii_alphabetic());
+            let all_valid = s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
+            if first_valid && all_valid {
+                return Some(s.to_string());
+            }
+        }
+    }
+    None
+}
+
+/**
+    Formats a Lua value into a pretty string.
+
+    This does not recursively format tables.
+*/
+pub(crate) fn format_value_styled(value: &LuaValue, prefer_plain: bool) -> String {
+    match value {
+        LuaValue::Nil => COLOR_YELLOW.apply_to("nil").to_string(),
+        LuaValue::Boolean(true) => COLOR_YELLOW.apply_to("true").to_string(),
+        LuaValue::Boolean(false) => COLOR_YELLOW.apply_to("false").to_string(),
+        LuaValue::Number(n) => COLOR_CYAN.apply_to(n).to_string(),
+        LuaValue::Integer(i) => COLOR_CYAN.apply_to(i).to_string(),
+        LuaValue::String(s) if prefer_plain => s.to_string_lossy().to_string(),
+        LuaValue::String(s) => COLOR_GREEN
+            .apply_to({
+                let mut s = s.to_string_lossy().to_string();
+                for (from, to) in STRING_REPLACEMENTS {
+                    s = s.replace(from, to);
+                }
+                format!(r#""{s}""#)
+            })
+            .to_string(),
+        LuaValue::Vector(_) => COLOR_MAGENTA.apply_to("<vector>").to_string(),
+        LuaValue::Thread(_) => COLOR_MAGENTA.apply_to("<thread>").to_string(),
+        LuaValue::Function(_) => COLOR_MAGENTA.apply_to("<function>").to_string(),
+        LuaValue::LightUserData(_) => COLOR_MAGENTA.apply_to("<pointer>").to_string(),
+        LuaValue::UserData(u) => {
+            if let Some(s) = call_userdata_tostring_metamethod(u) {
+                s
+            } else {
+                COLOR_MAGENTA.apply_to("<userdata>").to_string()
+            }
+        }
+        LuaValue::Table(t) => {
+            if let Some(s) = call_table_tostring_metamethod(t) {
+                s
+            } else {
+                COLOR_MAGENTA.apply_to("<table>").to_string()
+            }
+        }
+        _ => COLOR_MAGENTA.apply_to("<?>").to_string(),
+    }
+}
diff --git a/crates/lune-utils/src/fmt/value/config.rs b/crates/lune-utils/src/fmt/value/config.rs
new file mode 100644
index 0000000..ab1e950
--- /dev/null
+++ b/crates/lune-utils/src/fmt/value/config.rs
@@ -0,0 +1,48 @@
+/**
+    Configuration for formatting values.
+*/
+#[derive(Debug, Clone, Copy)]
+pub struct ValueFormatConfig {
+    pub(super) max_depth: usize,
+    pub(super) colors_enabled: bool,
+}
+
+impl ValueFormatConfig {
+    /**
+        Creates a new config with default values.
+    */
+    #[must_use]
+    pub const fn new() -> Self {
+        Self {
+            max_depth: 3,
+            colors_enabled: false,
+        }
+    }
+
+    /**
+        Sets the maximum depth to which tables will be formatted.
+    */
+    #[must_use]
+    pub const fn with_max_depth(self, max_depth: usize) -> Self {
+        Self { max_depth, ..self }
+    }
+
+    /**
+        Sets whether colors should be enabled.
+
+        Colors are disabled by default.
+    */
+    #[must_use]
+    pub const fn with_colors_enabled(self, colors_enabled: bool) -> Self {
+        Self {
+            colors_enabled,
+            ..self
+        }
+    }
+}
+
+impl Default for ValueFormatConfig {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/crates/lune-utils/src/fmt/value/metamethods.rs b/crates/lune-utils/src/fmt/value/metamethods.rs
new file mode 100644
index 0000000..8b00b1a
--- /dev/null
+++ b/crates/lune-utils/src/fmt/value/metamethods.rs
@@ -0,0 +1,29 @@
+use mlua::prelude::*;
+
+pub fn call_table_tostring_metamethod<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
+    let f = match tab.get_metatable() {
+        None => None,
+        Some(meta) => match meta.get::<_, LuaFunction>(LuaMetaMethod::ToString.name()) {
+            Ok(method) => Some(method),
+            Err(_) => None,
+        },
+    }?;
+    match f.call::<_, String>(()) {
+        Ok(res) => Some(res),
+        Err(_) => None,
+    }
+}
+
+pub fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
+    let f = match tab.get_metatable() {
+        Err(_) => None,
+        Ok(meta) => match meta.get::<LuaFunction>(LuaMetaMethod::ToString.name()) {
+            Ok(method) => Some(method),
+            Err(_) => None,
+        },
+    }?;
+    match f.call::<_, String>(()) {
+        Ok(res) => Some(res),
+        Err(_) => None,
+    }
+}
diff --git a/crates/lune-utils/src/fmt/value/mod.rs b/crates/lune-utils/src/fmt/value/mod.rs
new file mode 100644
index 0000000..3a3e310
--- /dev/null
+++ b/crates/lune-utils/src/fmt/value/mod.rs
@@ -0,0 +1,65 @@
+use std::{
+    collections::HashSet,
+    sync::{Arc, Mutex},
+};
+
+use console::{colors_enabled as get_colors_enabled, set_colors_enabled};
+use mlua::prelude::*;
+use once_cell::sync::Lazy;
+
+mod basic;
+mod config;
+mod metamethods;
+mod recursive;
+mod style;
+
+use self::recursive::format_value_recursive;
+
+pub use self::config::ValueFormatConfig;
+
+// NOTE: Since the setting for colors being enabled is global,
+// and these functions may be called in parallel, we use this global
+// lock to make sure that we don't mess up the colors for other threads.
+static COLORS_LOCK: Lazy<Arc<Mutex<()>>> = Lazy::new(|| Arc::new(Mutex::new(())));
+
+/**
+    Formats a Lua value into a pretty string using the given config.
+*/
+#[must_use]
+#[allow(clippy::missing_panics_doc)]
+pub fn pretty_format_value(value: &LuaValue, config: &ValueFormatConfig) -> String {
+    let _guard = COLORS_LOCK.lock().unwrap();
+
+    let were_colors_enabled = get_colors_enabled();
+    set_colors_enabled(were_colors_enabled && config.colors_enabled);
+
+    let mut visited = HashSet::new();
+    let res = format_value_recursive(value, config, &mut visited, 0);
+
+    set_colors_enabled(were_colors_enabled);
+    res.expect("using fmt for writing into strings should never fail")
+}
+
+/**
+    Formats a Lua multi-value into a pretty string using the given config.
+
+    Each value will be separated by a space.
+*/
+#[must_use]
+#[allow(clippy::missing_panics_doc)]
+pub fn pretty_format_multi_value(values: &LuaMultiValue, config: &ValueFormatConfig) -> String {
+    let _guard = COLORS_LOCK.lock().unwrap();
+
+    let were_colors_enabled = get_colors_enabled();
+    set_colors_enabled(were_colors_enabled && config.colors_enabled);
+
+    let mut visited = HashSet::new();
+    let res = values
+        .into_iter()
+        .map(|value| format_value_recursive(value, config, &mut visited, 0))
+        .collect::<Result<Vec<_>, _>>();
+
+    set_colors_enabled(were_colors_enabled);
+    res.expect("using fmt for writing into strings should never fail")
+        .join(" ")
+}
diff --git a/crates/lune-utils/src/fmt/value/recursive.rs b/crates/lune-utils/src/fmt/value/recursive.rs
new file mode 100644
index 0000000..7dfbef7
--- /dev/null
+++ b/crates/lune-utils/src/fmt/value/recursive.rs
@@ -0,0 +1,89 @@
+use std::collections::HashSet;
+use std::fmt::{self, Write as _};
+
+use mlua::prelude::*;
+
+use super::{
+    basic::{format_value_styled, lua_value_as_plain_string_key},
+    config::ValueFormatConfig,
+    style::STYLE_DIM,
+};
+
+const INDENT: &str = "    ";
+
+/**
+    Representation of a pointer in memory to a Lua value.
+*/
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub(crate) struct LuaValueId(usize);
+
+impl From<&LuaValue<'_>> for LuaValueId {
+    fn from(value: &LuaValue<'_>) -> Self {
+        Self(value.to_pointer() as usize)
+    }
+}
+
+impl From<&LuaTable<'_>> for LuaValueId {
+    fn from(table: &LuaTable) -> Self {
+        Self(table.to_pointer() as usize)
+    }
+}
+
+/**
+    Formats the given value, recursively formatting tables
+    up to the maximum depth specified in the config.
+
+    NOTE: We return a result here but it's really just to make handling
+    of the `write!` calls easier. Writing into a string should never fail.
+*/
+pub(crate) fn format_value_recursive(
+    value: &LuaValue,
+    config: &ValueFormatConfig,
+    visited: &mut HashSet<LuaValueId>,
+    depth: usize,
+) -> Result<String, fmt::Error> {
+    let mut buffer = String::new();
+
+    if let LuaValue::Table(ref t) = value {
+        if depth >= config.max_depth {
+            write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?;
+        } else if !visited.insert(LuaValueId::from(t)) {
+            write!(buffer, "{}", STYLE_DIM.apply_to("{ recursive }"))?;
+        } else {
+            writeln!(buffer, "{}", STYLE_DIM.apply_to("{"))?;
+
+            for res in t.clone().pairs::<LuaValue, LuaValue>() {
+                let (key, value) = res.expect("conversion to LuaValue should never fail");
+                let formatted = if let Some(plain_key) = lua_value_as_plain_string_key(&key) {
+                    format!(
+                        "{}{plain_key} {} {}{}",
+                        INDENT.repeat(1 + depth),
+                        STYLE_DIM.apply_to("="),
+                        format_value_recursive(&value, config, visited, depth + 1)?,
+                        STYLE_DIM.apply_to(","),
+                    )
+                } else {
+                    format!(
+                        "{}{}{}{} {} {}{}",
+                        INDENT.repeat(1 + depth),
+                        STYLE_DIM.apply_to("["),
+                        format_value_recursive(&key, config, visited, depth + 1)?,
+                        STYLE_DIM.apply_to("]"),
+                        STYLE_DIM.apply_to("="),
+                        format_value_recursive(&value, config, visited, depth + 1)?,
+                        STYLE_DIM.apply_to(","),
+                    )
+                };
+                buffer.push_str(&formatted);
+            }
+
+            visited.remove(&LuaValueId::from(t));
+            write!(buffer, "\n{}", STYLE_DIM.apply_to("}"))?;
+        }
+    } else {
+        let prefer_plain = depth == 0;
+        write!(buffer, "{}", format_value_styled(value, prefer_plain))?;
+    }
+
+    Ok(buffer)
+}
diff --git a/crates/lune-utils/src/fmt/value/style.rs b/crates/lune-utils/src/fmt/value/style.rs
new file mode 100644
index 0000000..0a4dbe4
--- /dev/null
+++ b/crates/lune-utils/src/fmt/value/style.rs
@@ -0,0 +1,9 @@
+use console::Style;
+use once_cell::sync::Lazy;
+
+pub static COLOR_GREEN: Lazy<Style> = Lazy::new(|| Style::new().green());
+pub static COLOR_YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow());
+pub static COLOR_MAGENTA: Lazy<Style> = Lazy::new(|| Style::new().magenta());
+pub static COLOR_CYAN: Lazy<Style> = Lazy::new(|| Style::new().cyan());
+
+pub static STYLE_DIM: Lazy<Style> = Lazy::new(|| Style::new().dim());
diff --git a/crates/lune-utils/src/lib.rs b/crates/lune-utils/src/lib.rs
new file mode 100644
index 0000000..52743a3
--- /dev/null
+++ b/crates/lune-utils/src/lib.rs
@@ -0,0 +1,10 @@
+#![allow(clippy::cargo_common_metadata)]
+
+mod table_builder;
+mod version_string;
+
+pub mod fmt;
+pub mod path;
+
+pub use self::table_builder::TableBuilder;
+pub use self::version_string::get_version_string;
diff --git a/crates/lune-utils/src/path.rs b/crates/lune-utils/src/path.rs
new file mode 100644
index 0000000..febaac6
--- /dev/null
+++ b/crates/lune-utils/src/path.rs
@@ -0,0 +1,101 @@
+use std::{
+    env::{current_dir, current_exe},
+    path::{Path, PathBuf, MAIN_SEPARATOR},
+    sync::Arc,
+};
+
+use once_cell::sync::Lazy;
+use path_clean::PathClean;
+
+static CWD: Lazy<Arc<Path>> = Lazy::new(create_cwd);
+static EXE: Lazy<Arc<Path>> = Lazy::new(create_exe);
+
+fn create_cwd() -> Arc<Path> {
+    let mut cwd = current_dir()
+        .expect("failed to find current working directory")
+        .to_str()
+        .expect("current working directory is not valid UTF-8")
+        .to_string();
+    if !cwd.ends_with(MAIN_SEPARATOR) {
+        cwd.push(MAIN_SEPARATOR);
+    }
+    dunce::canonicalize(cwd)
+        .expect("failed to canonicalize current working directory")
+        .into()
+}
+
+fn create_exe() -> Arc<Path> {
+    let exe = current_exe()
+        .expect("failed to find current executable")
+        .to_str()
+        .expect("current executable is not valid UTF-8")
+        .to_string();
+    dunce::canonicalize(exe)
+        .expect("failed to canonicalize current executable")
+        .into()
+}
+
+/**
+    Gets the current working directory as an absolute path.
+
+    This absolute path is canonicalized and does not contain any `.` or `..`
+    components, and it is also in a friendly (non-UNC) format.
+
+    This path is also guaranteed to:
+
+    - Be valid UTF-8.
+    - End with the platform's main path separator.
+*/
+#[must_use]
+pub fn get_current_dir() -> Arc<Path> {
+    Arc::clone(&CWD)
+}
+
+/**
+    Gets the path to the current executable as an absolute path.
+
+    This absolute path is canonicalized and does not contain any `.` or `..`
+    components, and it is also in a friendly (non-UNC) format.
+
+    This path is also guaranteed to:
+
+    - Be valid UTF-8.
+*/
+#[must_use]
+pub fn get_current_exe() -> Arc<Path> {
+    Arc::clone(&EXE)
+}
+
+/**
+    Diffs two paths against each other.
+
+    See the [`pathdiff`] crate for more information on what diffing paths does.
+*/
+pub fn diff_path(path: impl AsRef<Path>, base: impl AsRef<Path>) -> Option<PathBuf> {
+    pathdiff::diff_paths(path, base)
+}
+
+/**
+    Cleans a path.
+
+    See the [`path_clean`] crate for more information on what cleaning a path does.
+*/
+pub fn clean_path(path: impl AsRef<Path>) -> PathBuf {
+    path.as_ref().clean()
+}
+
+/**
+    Makes a path absolute and then cleans it.
+
+    Relative paths are resolved against the current working directory.
+
+    See the [`path_clean`] crate for more information on what cleaning a path does.
+*/
+pub fn clean_path_and_make_absolute(path: impl AsRef<Path>) -> PathBuf {
+    let path = path.as_ref();
+    if path.is_relative() {
+        CWD.join(path).clean()
+    } else {
+        path.clean()
+    }
+}
diff --git a/src/lune/util/table_builder.rs b/crates/lune-utils/src/table_builder.rs
similarity index 61%
rename from src/lune/util/table_builder.rs
rename to crates/lune-utils/src/table_builder.rs
index 88b9e4f..1678902 100644
--- a/src/lune/util/table_builder.rs
+++ b/crates/lune-utils/src/table_builder.rs
@@ -1,20 +1,31 @@
-#![allow(dead_code)]
+#![allow(clippy::missing_errors_doc)]
 
 use std::future::Future;
 
 use mlua::prelude::*;
 
+/**
+    Utility struct for building Lua tables.
+*/
 pub struct TableBuilder<'lua> {
     lua: &'lua Lua,
     tab: LuaTable<'lua>,
 }
 
 impl<'lua> TableBuilder<'lua> {
+    /**
+        Creates a new table builder.
+    */
     pub fn new(lua: &'lua Lua) -> LuaResult<Self> {
         let tab = lua.create_table()?;
         Ok(Self { lua, tab })
     }
 
+    /**
+        Adds a new key-value pair to the table.
+
+        This will overwrite any value that already exists.
+    */
     pub fn with_value<K, V>(self, key: K, value: V) -> LuaResult<Self>
     where
         K: IntoLua<'lua>,
@@ -24,6 +35,11 @@ impl<'lua> TableBuilder<'lua> {
         Ok(self)
     }
 
+    /**
+        Adds multiple key-value pairs to the table.
+
+        This will overwrite any values that already exist.
+    */
     pub fn with_values<K, V>(self, values: Vec<(K, V)>) -> LuaResult<Self>
     where
         K: IntoLua<'lua>,
@@ -35,6 +51,12 @@ impl<'lua> TableBuilder<'lua> {
         Ok(self)
     }
 
+    /**
+        Adds a new key-value pair to the sequential (array) section of the table.
+
+        This will not overwrite any value that already exists,
+        instead adding the value to the end of the array.
+    */
     pub fn with_sequential_value<V>(self, value: V) -> LuaResult<Self>
     where
         V: IntoLua<'lua>,
@@ -43,6 +65,12 @@ impl<'lua> TableBuilder<'lua> {
         Ok(self)
     }
 
+    /**
+        Adds multiple values to the sequential (array) section of the table.
+
+        This will not overwrite any values that already exist,
+        instead adding the values to the end of the array.
+    */
     pub fn with_sequential_values<V>(self, values: Vec<V>) -> LuaResult<Self>
     where
         V: IntoLua<'lua>,
@@ -53,6 +81,11 @@ impl<'lua> TableBuilder<'lua> {
         Ok(self)
     }
 
+    /**
+        Adds a new key-value pair to the table, with a function value.
+
+        This will overwrite any value that already exists.
+    */
     pub fn with_function<K, A, R, F>(self, key: K, func: F) -> LuaResult<Self>
     where
         K: IntoLua<'lua>,
@@ -64,20 +97,11 @@ impl<'lua> TableBuilder<'lua> {
         self.with_value(key, LuaValue::Function(f))
     }
 
-    pub fn with_metatable(self, table: LuaTable) -> LuaResult<Self> {
-        self.tab.set_metatable(Some(table));
-        Ok(self)
-    }
-
-    pub fn build_readonly(self) -> LuaResult<LuaTable<'lua>> {
-        self.tab.set_readonly(true);
-        Ok(self.tab)
-    }
-
-    pub fn build(self) -> LuaResult<LuaTable<'lua>> {
-        Ok(self.tab)
-    }
+    /**
+        Adds a new key-value pair to the table, with an async function value.
 
+        This will overwrite any value that already exists.
+    */
     pub fn with_async_function<K, A, R, F, FR>(self, key: K, func: F) -> LuaResult<Self>
     where
         K: IntoLua<'lua>,
@@ -89,4 +113,31 @@ impl<'lua> TableBuilder<'lua> {
         let f = self.lua.create_async_function(func)?;
         self.with_value(key, LuaValue::Function(f))
     }
+
+    /**
+        Adds a metatable to the table.
+
+        This will overwrite any metatable that already exists.
+    */
+    pub fn with_metatable(self, table: LuaTable) -> LuaResult<Self> {
+        self.tab.set_metatable(Some(table));
+        Ok(self)
+    }
+
+    /**
+        Builds the table as a read-only table.
+
+        This will prevent any *direct* modifications to the table.
+    */
+    pub fn build_readonly(self) -> LuaResult<LuaTable<'lua>> {
+        self.tab.set_readonly(true);
+        Ok(self.tab)
+    }
+
+    /**
+        Builds the table.
+    */
+    pub fn build(self) -> LuaResult<LuaTable<'lua>> {
+        Ok(self.tab)
+    }
 }
diff --git a/crates/lune-utils/src/version_string.rs b/crates/lune-utils/src/version_string.rs
new file mode 100644
index 0000000..6c4bbcf
--- /dev/null
+++ b/crates/lune-utils/src/version_string.rs
@@ -0,0 +1,75 @@
+use std::sync::Arc;
+
+use mlua::prelude::*;
+use once_cell::sync::Lazy;
+
+static LUAU_VERSION: Lazy<Arc<String>> = Lazy::new(create_luau_version_string);
+
+/**
+    Returns a Lune version string, in the format `Lune x.y.z+luau`.
+
+    The version string passed should be the version of the Lune runtime,
+    obtained from `env!("CARGO_PKG_VERSION")` or a similar mechanism.
+
+    # Panics
+
+    Panics if the version string is empty or contains invalid characters.
+*/
+#[must_use]
+pub fn get_version_string(lune_version: impl AsRef<str>) -> String {
+    let lune_version = lune_version.as_ref();
+
+    assert!(!lune_version.is_empty(), "Lune version string is empty");
+    assert!(
+        lune_version.chars().all(is_valid_version_char),
+        "Lune version string contains invalid characters"
+    );
+
+    format!("Lune {lune_version}+{}", *LUAU_VERSION)
+}
+
+fn create_luau_version_string() -> Arc<String> {
+    // Extract the current Luau version from a fresh Lua state / VM that can't be accessed externally.
+    let luau_version_full = {
+        let temp_lua = Lua::new();
+
+        let luau_version_full = temp_lua
+            .globals()
+            .get::<_, LuaString>("_VERSION")
+            .expect("Missing _VERSION global");
+
+        luau_version_full
+            .to_str()
+            .context("Invalid utf8 found in _VERSION global")
+            .expect("Expected _VERSION global to be a string")
+            .to_string()
+    };
+
+    // Luau version is expected to be in the format "Luau 0.x" and sometimes "Luau 0.x.y"
+    assert!(
+        luau_version_full.starts_with("Luau 0."),
+        "_VERSION global is formatted incorrectly\
+        \nFound string '{luau_version_full}'"
+    );
+    let luau_version_noprefix = luau_version_full.strip_prefix("Luau 0.").unwrap().trim();
+
+    // We make some guarantees about the format of the _VERSION global,
+    // so make sure that the luau version also follows those rules.
+    if luau_version_noprefix.is_empty() {
+        panic!(
+            "_VERSION global is missing version number\
+            \nFound string '{luau_version_full}'"
+        )
+    } else if !luau_version_noprefix.chars().all(is_valid_version_char) {
+        panic!(
+            "_VERSION global contains invalid characters\
+            \nFound string '{luau_version_full}'"
+        )
+    }
+
+    luau_version_noprefix.to_string().into()
+}
+
+fn is_valid_version_char(c: char) -> bool {
+    matches!(c, '0'..='9' | '.')
+}
diff --git a/crates/lune/Cargo.toml b/crates/lune/Cargo.toml
new file mode 100644
index 0000000..af528d9
--- /dev/null
+++ b/crates/lune/Cargo.toml
@@ -0,0 +1,89 @@
+[package]
+name = "lune"
+version = "0.8.3"
+edition = "2021"
+license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "A standalone Luau runtime"
+readme = "README.md"
+keywords = ["cli", "lua", "luau", "runtime"]
+categories = ["command-line-interface"]
+
+[[bin]]
+name = "lune"
+path = "src/main.rs"
+
+[lib]
+name = "lune"
+path = "src/lib.rs"
+
+[features]
+default = ["std", "cli"]
+
+std-datetime = ["dep:lune-std", "lune-std/datetime"]
+std-fs = ["dep:lune-std", "lune-std/fs"]
+std-luau = ["dep:lune-std", "lune-std/luau"]
+std-net = ["dep:lune-std", "lune-std/net"]
+std-process = ["dep:lune-std", "lune-std/process"]
+std-regex = ["dep:lune-std", "lune-std/regex"]
+std-roblox = ["dep:lune-std", "lune-std/roblox", "dep:lune-roblox"]
+std-serde = ["dep:lune-std", "lune-std/serde"]
+std-stdio = ["dep:lune-std", "lune-std/stdio"]
+std-task = ["dep:lune-std", "lune-std/task"]
+
+std = [
+    "std-datetime",
+    "std-fs",
+    "std-luau",
+    "std-net",
+    "std-process",
+    "std-regex",
+    "std-roblox",
+    "std-serde",
+    "std-stdio",
+    "std-task",
+]
+
+cli = [
+    "dep:env_logger",
+    "dep:clap",
+    "dep:include_dir",
+    "dep:rustyline",
+    "dep:zip_next",
+]
+
+[lints]
+workspace = true
+
+[dependencies]
+mlua = { version = "0.9.7", features = ["luau"] }
+mlua-luau-scheduler = "0.0.2"
+
+anyhow = "1.0"
+console = "0.15"
+dialoguer = "0.11"
+directories = "5.0"
+futures-util = "0.3"
+once_cell = "1.17"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+thiserror = "1.0"
+
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+tokio = { version = "1", features = ["full"] }
+reqwest = { version = "0.11", default-features = false, features = [
+    "rustls-tls",
+] }
+
+lune-std = { optional = true, version = "0.1.0", path = "../lune-std" }
+lune-roblox = { optional = true, version = "0.1.0", path = "../lune-roblox" }
+lune-utils = { version = "0.1.0", path = "../lune-utils" }
+
+### CLI
+
+env_logger = { optional = true, version = "0.11" }
+clap = { optional = true, version = "4.1", features = ["derive"] }
+include_dir = { optional = true, version = "0.7", features = ["glob"] }
+rustyline = { optional = true, version = "14.0" }
+zip_next = { optional = true, version = "1.1" }
diff --git a/src/cli/build/base_exe.rs b/crates/lune/src/cli/build/base_exe.rs
similarity index 100%
rename from src/cli/build/base_exe.rs
rename to crates/lune/src/cli/build/base_exe.rs
diff --git a/src/cli/build/files.rs b/crates/lune/src/cli/build/files.rs
similarity index 100%
rename from src/cli/build/files.rs
rename to crates/lune/src/cli/build/files.rs
diff --git a/src/cli/build/mod.rs b/crates/lune/src/cli/build/mod.rs
similarity index 100%
rename from src/cli/build/mod.rs
rename to crates/lune/src/cli/build/mod.rs
diff --git a/src/cli/build/result.rs b/crates/lune/src/cli/build/result.rs
similarity index 100%
rename from src/cli/build/result.rs
rename to crates/lune/src/cli/build/result.rs
diff --git a/src/cli/build/target.rs b/crates/lune/src/cli/build/target.rs
similarity index 96%
rename from src/cli/build/target.rs
rename to crates/lune/src/cli/build/target.rs
index 7ed9dd1..b7a3218 100644
--- a/src/cli/build/target.rs
+++ b/crates/lune/src/cli/build/target.rs
@@ -3,14 +3,14 @@ use std::{env::consts::ARCH, fmt, path::PathBuf, str::FromStr};
 use directories::BaseDirs;
 use once_cell::sync::Lazy;
 
-const HOME_DIR: Lazy<PathBuf> = Lazy::new(|| {
+static HOME_DIR: Lazy<PathBuf> = Lazy::new(|| {
     BaseDirs::new()
         .expect("could not find home directory")
         .home_dir()
         .to_path_buf()
 });
 
-pub const CACHE_DIR: Lazy<PathBuf> = Lazy::new(|| HOME_DIR.join(".lune").join("target"));
+pub static CACHE_DIR: Lazy<PathBuf> = Lazy::new(|| HOME_DIR.join(".lune").join("target"));
 
 /**
     A target operating system supported by Lune
diff --git a/src/cli/list.rs b/crates/lune/src/cli/list.rs
similarity index 100%
rename from src/cli/list.rs
rename to crates/lune/src/cli/list.rs
diff --git a/src/cli/mod.rs b/crates/lune/src/cli/mod.rs
similarity index 100%
rename from src/cli/mod.rs
rename to crates/lune/src/cli/mod.rs
diff --git a/src/cli/repl.rs b/crates/lune/src/cli/repl.rs
similarity index 100%
rename from src/cli/repl.rs
rename to crates/lune/src/cli/repl.rs
diff --git a/src/cli/run.rs b/crates/lune/src/cli/run.rs
similarity index 100%
rename from src/cli/run.rs
rename to crates/lune/src/cli/run.rs
diff --git a/src/cli/setup.rs b/crates/lune/src/cli/setup.rs
similarity index 100%
rename from src/cli/setup.rs
rename to crates/lune/src/cli/setup.rs
diff --git a/src/cli/utils/files.rs b/crates/lune/src/cli/utils/files.rs
similarity index 96%
rename from src/cli/utils/files.rs
rename to crates/lune/src/cli/utils/files.rs
index 9b9b1ca..2e02bb8 100644
--- a/src/cli/utils/files.rs
+++ b/crates/lune/src/cli/utils/files.rs
@@ -6,7 +6,6 @@ use std::{
 use anyhow::{anyhow, bail, Result};
 use console::style;
 use directories::UserDirs;
-use itertools::Itertools;
 use once_cell::sync::Lazy;
 
 const LUNE_COMMENT_PREFIX: &str = "-->";
@@ -180,10 +179,9 @@ pub fn parse_lune_description_from_file(contents: &str) -> Option<String> {
         });
         let unindented_lines = comment_lines
             .iter()
-            .map(|line| &line[shortest_indent..])
-            // Replace newlines with a single space inbetween instead
-            .interleave(std::iter::repeat(" ").take(comment_lines.len() - 1))
-            .collect();
+            .map(|line| line[shortest_indent..].to_string())
+            .collect::<Vec<_>>()
+            .join(" ");
         Some(unindented_lines)
     }
 }
diff --git a/src/cli/utils/listing.rs b/crates/lune/src/cli/utils/listing.rs
similarity index 100%
rename from src/cli/utils/listing.rs
rename to crates/lune/src/cli/utils/listing.rs
diff --git a/src/cli/utils/mod.rs b/crates/lune/src/cli/utils/mod.rs
similarity index 100%
rename from src/cli/utils/mod.rs
rename to crates/lune/src/cli/utils/mod.rs
diff --git a/crates/lune/src/lib.rs b/crates/lune/src/lib.rs
new file mode 100644
index 0000000..1a53abb
--- /dev/null
+++ b/crates/lune/src/lib.rs
@@ -0,0 +1,11 @@
+#![allow(clippy::cargo_common_metadata)]
+
+mod rt;
+
+#[cfg(feature = "std-roblox")]
+pub use lune_roblox as roblox;
+
+#[cfg(test)]
+mod tests;
+
+pub use crate::rt::{Runtime, RuntimeError, RuntimeResult};
diff --git a/crates/lune/src/main.rs b/crates/lune/src/main.rs
new file mode 100644
index 0000000..5d04a63
--- /dev/null
+++ b/crates/lune/src/main.rs
@@ -0,0 +1,42 @@
+#![allow(clippy::cargo_common_metadata)]
+
+use std::process::ExitCode;
+
+#[cfg(feature = "cli")]
+pub(crate) mod cli;
+
+pub(crate) mod standalone;
+
+use lune_utils::fmt::Label;
+
+#[tokio::main(flavor = "multi_thread")]
+async fn main() -> ExitCode {
+    tracing_subscriber::fmt()
+        .compact()
+        .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
+        .with_target(true)
+        .with_timer(tracing_subscriber::fmt::time::uptime())
+        .with_level(true)
+        .init();
+
+    if let Some(bin) = standalone::check().await {
+        return standalone::run(bin).await.unwrap();
+    }
+
+    #[cfg(feature = "cli")]
+    {
+        match cli::Cli::new().run().await {
+            Ok(code) => code,
+            Err(err) => {
+                eprintln!("{}\n{err:?}", Label::Error);
+                ExitCode::FAILURE
+            }
+        }
+    }
+
+    #[cfg(not(feature = "cli"))]
+    {
+        eprintln!("{}\nCLI feature is disabled", Label::Error);
+        ExitCode::FAILURE
+    }
+}
diff --git a/crates/lune/src/rt/mod.rs b/crates/lune/src/rt/mod.rs
new file mode 100644
index 0000000..16393f1
--- /dev/null
+++ b/crates/lune/src/rt/mod.rs
@@ -0,0 +1,5 @@
+mod result;
+mod runtime;
+
+pub use self::result::{RuntimeError, RuntimeResult};
+pub use self::runtime::Runtime;
diff --git a/src/lune/error.rs b/crates/lune/src/rt/result.rs
similarity index 89%
rename from src/lune/error.rs
rename to crates/lune/src/rt/result.rs
index 840913d..f3c9c3a 100644
--- a/src/lune/error.rs
+++ b/crates/lune/src/rt/result.rs
@@ -5,7 +5,9 @@ use std::{
 
 use mlua::prelude::*;
 
-use crate::lune::util::formatting::pretty_format_luau_error;
+use lune_utils::fmt::ErrorComponents;
+
+pub type RuntimeResult<T, E = RuntimeError> = Result<T, E>;
 
 /**
     An opaque error type for formatted lua errors.
@@ -22,6 +24,7 @@ impl RuntimeError {
 
         Colorization is enabled by default.
     */
+    #[must_use]
     #[doc(hidden)]
     pub fn enable_colors(mut self) -> Self {
         self.disable_colors = false;
@@ -33,6 +36,7 @@ impl RuntimeError {
 
         Colorization is enabled by default.
     */
+    #[must_use]
     #[doc(hidden)]
     pub fn disable_colors(mut self) -> Self {
         self.disable_colors = true;
@@ -44,6 +48,7 @@ impl RuntimeError {
 
         See [`mlua::Error::SyntaxError`] for more information.
     */
+    #[must_use]
     pub fn is_incomplete_input(&self) -> bool {
         matches!(
             self.error,
@@ -75,11 +80,7 @@ impl From<&LuaError> for RuntimeError {
 
 impl Display for RuntimeError {
     fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
-        write!(
-            f,
-            "{}",
-            pretty_format_luau_error(&self.error, !self.disable_colors)
-        )
+        write!(f, "{}", ErrorComponents::from(self.error.clone()))
     }
 }
 
diff --git a/src/lune/mod.rs b/crates/lune/src/rt/runtime.rs
similarity index 73%
rename from src/lune/mod.rs
rename to crates/lune/src/rt/runtime.rs
index ce676bf..a5501c6 100644
--- a/src/lune/mod.rs
+++ b/crates/lune/src/rt/runtime.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::missing_panics_doc)]
+
 use std::{
     process::ExitCode,
     rc::Rc,
@@ -10,13 +12,7 @@ use std::{
 use mlua::Lua;
 use mlua_luau_scheduler::Scheduler;
 
-mod builtins;
-mod error;
-mod globals;
-
-pub(crate) mod util;
-
-pub use error::RuntimeError;
+use super::{RuntimeError, RuntimeResult};
 
 #[derive(Debug)]
 pub struct Runtime {
@@ -27,7 +23,10 @@ pub struct Runtime {
 impl Runtime {
     /**
         Creates a new Lune runtime, with a new Luau VM.
+
+        Injects standard globals and libraries if any of the `std` features are enabled.
     */
+    #[must_use]
     #[allow(clippy::new_without_default)]
     pub fn new() -> Self {
         let lua = Rc::new(Lua::new());
@@ -35,7 +34,21 @@ impl Runtime {
         lua.set_app_data(Rc::downgrade(&lua));
         lua.set_app_data(Vec::<String>::new());
 
-        globals::inject_all(&lua).expect("Failed to inject globals");
+        #[cfg(any(
+            feature = "std-datetime",
+            feature = "std-fs",
+            feature = "std-luau",
+            feature = "std-net",
+            feature = "std-process",
+            feature = "std-regex",
+            feature = "std-roblox",
+            feature = "std-serde",
+            feature = "std-stdio",
+            feature = "std-task",
+        ))]
+        {
+            lune_std::inject_globals(&lua).expect("Failed to inject globals");
+        }
 
         Self {
             lua,
@@ -46,6 +59,7 @@ impl Runtime {
     /**
         Sets arguments to give in `process.args` for Lune scripts.
     */
+    #[must_use]
     pub fn with_args<V>(mut self, args: V) -> Self
     where
         V: Into<Vec<String>>,
@@ -59,12 +73,16 @@ impl Runtime {
         Runs a Lune script inside of the current runtime.
 
         This will preserve any modifications to global values / context.
+
+        # Errors
+
+        This function will return an error if the script fails to run.
     */
     pub async fn run(
         &mut self,
         script_name: impl AsRef<str>,
         script_contents: impl AsRef<[u8]>,
-    ) -> Result<ExitCode, RuntimeError> {
+    ) -> RuntimeResult<ExitCode> {
         // Create a new scheduler for this run
         let sched = Scheduler::new(&self.lua);
 
diff --git a/src/standalone/metadata.rs b/crates/lune/src/standalone/metadata.rs
similarity index 98%
rename from src/standalone/metadata.rs
rename to crates/lune/src/standalone/metadata.rs
index f2ddfd9..e625d47 100644
--- a/src/standalone/metadata.rs
+++ b/crates/lune/src/standalone/metadata.rs
@@ -5,7 +5,7 @@ use mlua::Compiler as LuaCompiler;
 use once_cell::sync::Lazy;
 use tokio::fs;
 
-pub const CURRENT_EXE: Lazy<PathBuf> =
+pub static CURRENT_EXE: Lazy<PathBuf> =
     Lazy::new(|| env::current_exe().expect("failed to get current exe"));
 const MAGIC: &[u8; 8] = b"cr3sc3nt";
 
diff --git a/src/standalone/mod.rs b/crates/lune/src/standalone/mod.rs
similarity index 100%
rename from src/standalone/mod.rs
rename to crates/lune/src/standalone/mod.rs
diff --git a/src/standalone/tracer.rs b/crates/lune/src/standalone/tracer.rs
similarity index 100%
rename from src/standalone/tracer.rs
rename to crates/lune/src/standalone/tracer.rs
diff --git a/src/tests.rs b/crates/lune/src/tests.rs
similarity index 86%
rename from src/tests.rs
rename to crates/lune/src/tests.rs
index b14566f..ae599da 100644
--- a/src/tests.rs
+++ b/crates/lune/src/tests.rs
@@ -1,3 +1,5 @@
+use std::env::set_current_dir;
+use std::path::PathBuf;
 use std::process::ExitCode;
 
 use anyhow::Result;
@@ -5,6 +7,8 @@ use console::set_colors_enabled;
 use console::set_colors_enabled_stderr;
 use tokio::fs::read_to_string;
 
+use lune_utils::path::clean_path_and_make_absolute;
+
 use crate::Runtime;
 
 const ARGS: &[&str] = &["Foo", "Bar"];
@@ -13,12 +17,19 @@ macro_rules! create_tests {
     ($($name:ident: $value:expr,)*) => { $(
         #[tokio::test(flavor = "multi_thread")]
         async fn $name() -> Result<ExitCode> {
+            // We need to change the current directory to the workspace root since
+            // we are in a sub-crate and tests would run relative to the sub-crate
+            let workspace_dir_str = format!("{}/../../", env!("CARGO_MANIFEST_DIR"));
+            let workspace_dir = clean_path_and_make_absolute(PathBuf::from(workspace_dir_str));
+            set_current_dir(&workspace_dir)?;
+
             // Disable styling for stdout and stderr since
             // some tests rely on output not being styled
             set_colors_enabled(false);
             set_colors_enabled_stderr(false);
+
             // The rest of the test logic can continue as normal
-            let full_name = format!("tests/{}.luau", $value);
+            let full_name = format!("{}/tests/{}.luau", workspace_dir.display(), $value);
             let script = read_to_string(&full_name).await?;
             let mut lune = Runtime::new().with_args(
                 ARGS
@@ -37,56 +48,19 @@ macro_rules! create_tests {
     )* }
 }
 
+#[cfg(any(
+    feature = "std-datetime",
+    feature = "std-fs",
+    feature = "std-luau",
+    feature = "std-net",
+    feature = "std-process",
+    feature = "std-regex",
+    feature = "std-roblox",
+    feature = "std-serde",
+    feature = "std-stdio",
+    feature = "std-task",
+))]
 create_tests! {
-    datetime_format_local_time: "datetime/formatLocalTime",
-    datetime_format_universal_time: "datetime/formatUniversalTime",
-    datetime_from_iso_date: "datetime/fromIsoDate",
-    datetime_from_local_time: "datetime/fromLocalTime",
-    datetime_from_universal_time: "datetime/fromUniversalTime",
-    datetime_from_unix_timestamp: "datetime/fromUnixTimestamp",
-    datetime_now: "datetime/now",
-    datetime_to_iso_date: "datetime/toIsoDate",
-    datetime_to_local_time: "datetime/toLocalTime",
-    datetime_to_universal_time: "datetime/toUniversalTime",
-
-    fs_files: "fs/files",
-    fs_copy: "fs/copy",
-    fs_dirs: "fs/dirs",
-    fs_metadata: "fs/metadata",
-    fs_move: "fs/move",
-
-    luau_compile: "luau/compile",
-    luau_load: "luau/load",
-    luau_options: "luau/options",
-
-    net_request_codes: "net/request/codes",
-    net_request_compression: "net/request/compression",
-    net_request_methods: "net/request/methods",
-    net_request_query: "net/request/query",
-    net_request_redirect: "net/request/redirect",
-    net_url_encode: "net/url/encode",
-    net_url_decode: "net/url/decode",
-    net_serve_requests: "net/serve/requests",
-    net_serve_websockets: "net/serve/websockets",
-    net_socket_basic: "net/socket/basic",
-    net_socket_wss: "net/socket/wss",
-    net_socket_wss_rw: "net/socket/wss_rw",
-
-    process_args: "process/args",
-    process_cwd: "process/cwd",
-    process_env: "process/env",
-    process_exit: "process/exit",
-    process_spawn_async: "process/spawn/async",
-    process_spawn_basic: "process/spawn/basic",
-    process_spawn_cwd: "process/spawn/cwd",
-    process_spawn_shell: "process/spawn/shell",
-    process_spawn_stdin: "process/spawn/stdin",
-    process_spawn_stdio: "process/spawn/stdio",
-
-    regex_general: "regex/general",
-    regex_metamethods: "regex/metamethods",
-    regex_replace: "regex/replace",
-
     require_aliases: "require/tests/aliases",
     require_async: "require/tests/async",
     require_async_concurrent: "require/tests/async_concurrent",
@@ -109,28 +83,76 @@ create_tests! {
     global_type: "globals/type",
     global_typeof: "globals/typeof",
     global_warn: "globals/warn",
-
-    serde_compression_files: "serde/compression/files",
-    serde_compression_roundtrip: "serde/compression/roundtrip",
-    serde_json_decode: "serde/json/decode",
-    serde_json_encode: "serde/json/encode",
-    serde_toml_decode: "serde/toml/decode",
-    serde_toml_encode: "serde/toml/encode",
-
-    stdio_format: "stdio/format",
-    stdio_color: "stdio/color",
-    stdio_style: "stdio/style",
-    stdio_write: "stdio/write",
-    stdio_ewrite: "stdio/ewrite",
-
-    task_cancel: "task/cancel",
-    task_defer: "task/defer",
-    task_delay: "task/delay",
-    task_spawn: "task/spawn",
-    task_wait: "task/wait",
 }
 
-#[cfg(feature = "roblox")]
+#[cfg(feature = "std-datetime")]
+create_tests! {
+    datetime_format_local_time: "datetime/formatLocalTime",
+    datetime_format_universal_time: "datetime/formatUniversalTime",
+    datetime_from_iso_date: "datetime/fromIsoDate",
+    datetime_from_local_time: "datetime/fromLocalTime",
+    datetime_from_universal_time: "datetime/fromUniversalTime",
+    datetime_from_unix_timestamp: "datetime/fromUnixTimestamp",
+    datetime_now: "datetime/now",
+    datetime_to_iso_date: "datetime/toIsoDate",
+    datetime_to_local_time: "datetime/toLocalTime",
+    datetime_to_universal_time: "datetime/toUniversalTime",
+}
+
+#[cfg(feature = "std-fs")]
+create_tests! {
+    fs_files: "fs/files",
+    fs_copy: "fs/copy",
+    fs_dirs: "fs/dirs",
+    fs_metadata: "fs/metadata",
+    fs_move: "fs/move",
+}
+
+#[cfg(feature = "std-luau")]
+create_tests! {
+    luau_compile: "luau/compile",
+    luau_load: "luau/load",
+    luau_options: "luau/options",
+}
+
+#[cfg(feature = "std-net")]
+create_tests! {
+    net_request_codes: "net/request/codes",
+    net_request_compression: "net/request/compression",
+    net_request_methods: "net/request/methods",
+    net_request_query: "net/request/query",
+    net_request_redirect: "net/request/redirect",
+    net_url_encode: "net/url/encode",
+    net_url_decode: "net/url/decode",
+    net_serve_requests: "net/serve/requests",
+    net_serve_websockets: "net/serve/websockets",
+    net_socket_basic: "net/socket/basic",
+    net_socket_wss: "net/socket/wss",
+    net_socket_wss_rw: "net/socket/wss_rw",
+}
+
+#[cfg(feature = "std-process")]
+create_tests! {
+    process_args: "process/args",
+    process_cwd: "process/cwd",
+    process_env: "process/env",
+    process_exit: "process/exit",
+    process_spawn_async: "process/spawn/async",
+    process_spawn_basic: "process/spawn/basic",
+    process_spawn_cwd: "process/spawn/cwd",
+    process_spawn_shell: "process/spawn/shell",
+    process_spawn_stdin: "process/spawn/stdin",
+    process_spawn_stdio: "process/spawn/stdio",
+}
+
+#[cfg(feature = "std-regex")]
+create_tests! {
+    regex_general: "regex/general",
+    regex_metamethods: "regex/metamethods",
+    regex_replace: "regex/replace",
+}
+
+#[cfg(feature = "std-roblox")]
 create_tests! {
     roblox_datatype_axes: "roblox/datatypes/Axes",
     roblox_datatype_brick_color: "roblox/datatypes/BrickColor",
@@ -198,3 +220,31 @@ create_tests! {
     roblox_reflection_enums: "roblox/reflection/enums",
     roblox_reflection_property: "roblox/reflection/property",
 }
+
+#[cfg(feature = "std-serde")]
+create_tests! {
+    serde_compression_files: "serde/compression/files",
+    serde_compression_roundtrip: "serde/compression/roundtrip",
+    serde_json_decode: "serde/json/decode",
+    serde_json_encode: "serde/json/encode",
+    serde_toml_decode: "serde/toml/decode",
+    serde_toml_encode: "serde/toml/encode",
+}
+
+#[cfg(feature = "std-stdio")]
+create_tests! {
+    stdio_format: "stdio/format",
+    stdio_color: "stdio/color",
+    stdio_style: "stdio/style",
+    stdio_write: "stdio/write",
+    stdio_ewrite: "stdio/ewrite",
+}
+
+#[cfg(feature = "std-task")]
+create_tests! {
+    task_cancel: "task/cancel",
+    task_defer: "task/defer",
+    task_delay: "task/delay",
+    task_spawn: "task/spawn",
+    task_wait: "task/wait",
+}
diff --git a/scripts/generate_compression_test_files.luau b/scripts/generate_compression_test_files.luau
new file mode 100644
index 0000000..954929a
--- /dev/null
+++ b/scripts/generate_compression_test_files.luau
@@ -0,0 +1,303 @@
+local fs = require("@lune/fs")
+local process = require("@lune/process")
+local serde = require("@lune/serde")
+local stdio = require("@lune/stdio")
+
+local TEST_FILES_DIR = process.cwd .. "tests/serde/test-files"
+local INPUT_FILE = TEST_FILES_DIR .. "/loremipsum.txt"
+local TEMP_FILE = TEST_FILES_DIR .. "/loremipsum.temp"
+
+local INPUT_FILE_CONTENTS = fs.readFile(INPUT_FILE)
+
+-- Make some utility functions for viewing unexpected differences in files easier
+
+local function stringAsHex(str: string): string
+	local hex = {}
+	for i = 1, #str do
+		table.insert(hex, string.format("%02x", string.byte(str, i)))
+	end
+	return table.concat(hex)
+end
+
+local function hexDiff(a: string, b: string): string
+	local diff = {}
+	for i = 1, math.max(#a, #b) do
+		local aByte = if #a >= i then string.byte(a, i) else nil
+		local bByte = if #b >= i then string.byte(b, i) else nil
+		if aByte == nil then
+			table.insert(
+				diff,
+				string.format(
+					"%s%02x%s",
+					stdio.color("green"),
+					assert(bByte, "unreachable"),
+					stdio.color("reset")
+				)
+			)
+		elseif bByte == nil then
+			table.insert(
+				diff,
+				string.format(
+					"%s%02x%s",
+					stdio.color("red"),
+					assert(aByte, "unreachable"),
+					stdio.color("reset")
+				)
+			)
+		else
+			if aByte == bByte then
+				table.insert(diff, string.format("%02x", aByte))
+			else
+				table.insert(
+					diff,
+					string.format(
+						"%s%02x%s",
+						stdio.color("yellow"),
+						assert(bByte, "unreachable"),
+						stdio.color("reset")
+					)
+				)
+			end
+		end
+	end
+	return table.concat(diff)
+end
+
+local function stripCwdIfPresent(path: string): string
+	if string.sub(path, 1, #process.cwd) == process.cwd then
+		return string.sub(path, #process.cwd + 1)
+	else
+		return path
+	end
+end
+
+-- Make some processing functions for manipulating output of certain commands
+
+local function processNoop(output: string): string
+	return output
+end
+
+local function processGzipSetOsUnknown(output: string): string
+	-- This will set the os bits to be "unknown" so that the
+	-- output is deterministic and consistent with serde lib
+	-- https://www.rfc-editor.org/rfc/rfc1952#section-2.3.1
+	local buf = buffer.fromstring(output)
+	buffer.writeu8(buf, 9, 0xFF)
+	return buffer.tostring(buf)
+end
+
+local function processLz4PrependSize(output: string): string
+	-- Lune supports only lz4 with the decompressed size
+	-- prepended to it, but the lz4 command line tool
+	-- doesn't add this automatically, so we have to
+	-- TODO: Remove this in the future when no longer needed
+	local buf = buffer.create(4 + #output)
+	buffer.writeu32(buf, 0, #INPUT_FILE_CONTENTS)
+	buffer.writestring(buf, 4, output)
+	return buffer.tostring(buf)
+end
+
+-- Make sure we have all of the different compression tools installed,
+-- note that on macos we do not use the system-installed compression
+-- tools, instead preferring to use homebrew-installed (gnu) ones
+
+local BIN_BROTLI = if process.os == "macos" then "/opt/homebrew/bin/brotli" else "brotli"
+local BIN_GZIP = if process.os == "macos" then "/opt/homebrew/bin/gzip" else "gzip"
+local BIN_LZ4 = if process.os == "macos" then "/opt/homebrew/bin/lz4" else "lz4"
+local BIN_ZLIB = if process.os == "macos" then "/opt/homebrew/bin/pigz" else "pigz"
+
+local function checkInstalled(program: string, args: { string }?)
+	print("Checking if", program, "is installed")
+	local result = process.spawn(program, args)
+	if not result.ok then
+		stdio.ewrite(string.format("Program '%s' is not installed\n", program))
+		process.exit(1)
+	end
+end
+
+checkInstalled(BIN_BROTLI, { "--version" })
+checkInstalled(BIN_GZIP, { "--version" })
+checkInstalled(BIN_LZ4, { "--version" })
+checkInstalled(BIN_ZLIB, { "--version" })
+
+-- Run them to generate files
+
+local function run(program: string, args: { string }): string
+	local result = process.spawn(program, args)
+	if not result.ok then
+		stdio.ewrite(string.format("Command '%s' failed\n", program))
+		if #result.stdout > 0 then
+			stdio.ewrite("stdout:\n")
+			stdio.ewrite(result.stdout)
+			stdio.ewrite("\n")
+		end
+		if #result.stderr > 0 then
+			stdio.ewrite("stderr:\n")
+			stdio.ewrite(result.stderr)
+			stdio.ewrite("\n")
+		end
+		process.exit(1)
+	else
+		if #result.stdout > 0 then
+			stdio.ewrite("stdout:\n")
+			stdio.ewrite(result.stdout)
+			stdio.ewrite("\n")
+		end
+	end
+	return result.stdout
+end
+
+local OUTPUT_FILES = {
+	{
+		command = BIN_BROTLI,
+		format = "brotli" :: serde.CompressDecompressFormat,
+		args = { "--best", "-w", "22", TEMP_FILE },
+		output = TEMP_FILE .. ".br",
+		process = processNoop,
+		final = INPUT_FILE .. ".br",
+	},
+	{
+		command = BIN_GZIP,
+		format = "gzip" :: serde.CompressDecompressFormat,
+		args = { "--best", "--no-name", "--synchronous", TEMP_FILE },
+		output = TEMP_FILE .. ".gz",
+		process = processGzipSetOsUnknown,
+		final = INPUT_FILE .. ".gz",
+	},
+	{
+		command = BIN_LZ4,
+		format = "lz4" :: serde.CompressDecompressFormat,
+		args = { "--best", TEMP_FILE, TEMP_FILE .. ".lz4" },
+		output = TEMP_FILE .. ".lz4",
+		process = processLz4PrependSize,
+		final = INPUT_FILE .. ".lz4",
+	},
+	{
+		command = BIN_ZLIB,
+		format = "zlib" :: serde.CompressDecompressFormat,
+		args = { "--best", "--zlib", TEMP_FILE },
+		output = TEMP_FILE .. ".zz",
+		process = processNoop,
+		final = INPUT_FILE .. ".z",
+	},
+}
+
+for _, spec in OUTPUT_FILES do
+	-- Write the temp file for the compression tool to read and use, then
+	-- remove it, some tools may remove it on their own, so we ignore errors
+	fs.writeFile(TEMP_FILE, INPUT_FILE_CONTENTS)
+	local argsToDisplay = {}
+	for _, arg in spec.args do
+		table.insert(argsToDisplay, stripCwdIfPresent(arg))
+	end
+	print(
+		"\nRunning compression\n  Cmd:  ",
+		spec.command,
+		"\n  Args: ",
+		stdio.format(table.unpack(argsToDisplay))
+	)
+	local output = run(spec.command, spec.args)
+	if #output > 0 then
+		print("Output:", output)
+	end
+	pcall(fs.removeFile, TEMP_FILE)
+
+	-- Read the compressed output file that is now supposed to exist
+	local compressedContents
+	pcall(function()
+		compressedContents = fs.readFile(spec.output)
+		compressedContents = spec.process(compressedContents)
+		fs.removeFile(spec.output)
+	end)
+	if not compressedContents then
+		error(
+			string.format(
+				"Nothing was written to output file while running %s:\n%s",
+				spec.command,
+				spec.output
+			)
+		)
+	end
+
+	-- If the newly compressed contents do not match the existing contents,
+	-- warn the user about this and ask if they want to overwrite the file
+	local existingContents = fs.readFile(spec.final)
+	if compressedContents ~= existingContents then
+		stdio.ewrite("\nCompressed file does not match existing contents!")
+		stdio.ewrite("\n\nExisting:\n")
+		stdio.ewrite(stringAsHex(existingContents))
+		stdio.ewrite("\n\nCompressed:\n")
+		stdio.ewrite(hexDiff(existingContents, compressedContents))
+		stdio.ewrite("\n\n")
+		local confirm = stdio.prompt("confirm", "Do you want to continue?")
+		if confirm == true then
+			print("Overwriting file!")
+		else
+			stdio.ewrite("\n\nAborting...\n")
+			process.exit(1)
+			return
+		end
+	end
+
+	-- Check if the compressed contents can be decompressed using serde
+	local decompressSuccess, decompressedContents =
+		pcall(serde.decompress, spec.format, compressedContents)
+	if not decompressSuccess then
+		stdio.ewrite("\nCompressed contents could not be decompressed using serde!")
+		stdio.ewrite("\n\nCompressed:\n")
+		stdio.ewrite(stringAsHex(compressedContents))
+		stdio.ewrite("\n\nError:\n")
+		stdio.ewrite(tostring(decompressedContents))
+		stdio.ewrite("\n\n")
+		local confirm = stdio.prompt("confirm", "Do you want to continue?")
+		if confirm == true then
+			print("Ignoring decompression error!")
+		else
+			stdio.ewrite("\n\nAborting...\n")
+			process.exit(1)
+			return
+		end
+	end
+	if decompressedContents ~= INPUT_FILE_CONTENTS then
+		stdio.ewrite("\nCompressed contents were not decompressable properly using serde!")
+		stdio.ewrite("\n\nOriginal:\n")
+		stdio.ewrite(INPUT_FILE_CONTENTS)
+		stdio.ewrite("\n\nDecompressed:\n")
+		stdio.ewrite(decompressedContents)
+		stdio.ewrite("\n\n")
+		local confirm = stdio.prompt("confirm", "Do you want to continue?")
+		if confirm == true then
+			print("Ignoring decompression mismatch!")
+		else
+			stdio.ewrite("\n\nAborting...\n")
+			process.exit(1)
+			return
+		end
+	end
+
+	-- Check if the compressed contents match the serde compressed contents,
+	-- if they don't this will 100% make the tests fail, but maybe we are doing
+	-- it because we are updating the serde library and need to update test files
+	local serdeContents = serde.compress(spec.format, INPUT_FILE_CONTENTS)
+	if compressedContents ~= serdeContents then
+		stdio.ewrite("\nTemp file does not match contents compressed with serde!")
+		stdio.ewrite("\nThis will caused the new compressed file to fail tests.")
+		stdio.ewrite("\n\nSerde:\n")
+		stdio.ewrite(stringAsHex(serdeContents))
+		stdio.ewrite("\n\nCompressed:\n")
+		stdio.ewrite(hexDiff(serdeContents, compressedContents))
+		stdio.ewrite("\n\n")
+		local confirm = stdio.prompt("confirm", "Do you want to continue?")
+		if confirm == true then
+			print("Writing new file!")
+		else
+			stdio.ewrite("\n\nAborting...\n")
+			process.exit(1)
+			return
+		end
+	end
+
+	-- Finally, write the new compressed file
+	fs.writeFile(spec.final, compressedContents)
+	print("Wrote new file successfully to", stripCwdIfPresent(spec.final))
+end
diff --git a/src/lib.rs b/src/lib.rs
deleted file mode 100644
index eaaa157..0000000
--- a/src/lib.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-mod lune;
-
-#[cfg(feature = "roblox")]
-pub mod roblox;
-
-#[cfg(test)]
-mod tests;
-
-pub use crate::lune::{Runtime, RuntimeError};
diff --git a/src/lune/builtins/mod.rs b/src/lune/builtins/mod.rs
deleted file mode 100644
index 8006a80..0000000
--- a/src/lune/builtins/mod.rs
+++ /dev/null
@@ -1,92 +0,0 @@
-use std::str::FromStr;
-
-use mlua::prelude::*;
-
-mod datetime;
-mod fs;
-mod luau;
-mod net;
-mod process;
-mod regex;
-mod serde;
-mod stdio;
-mod task;
-
-#[cfg(feature = "roblox")]
-mod roblox;
-
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
-pub enum LuneBuiltin {
-    DateTime,
-    Fs,
-    Luau,
-    Net,
-    Task,
-    Process,
-    Regex,
-    Serde,
-    Stdio,
-    #[cfg(feature = "roblox")]
-    Roblox,
-}
-
-impl LuneBuiltin {
-    pub fn name(&self) -> &'static str {
-        match self {
-            Self::DateTime => "datetime",
-            Self::Fs => "fs",
-            Self::Luau => "luau",
-            Self::Net => "net",
-            Self::Task => "task",
-            Self::Process => "process",
-            Self::Regex => "regex",
-            Self::Serde => "serde",
-            Self::Stdio => "stdio",
-            #[cfg(feature = "roblox")]
-            Self::Roblox => "roblox",
-        }
-    }
-
-    pub fn create<'lua>(&self, lua: &'lua Lua) -> LuaResult<LuaMultiValue<'lua>> {
-        let res = match self {
-            Self::DateTime => datetime::create(lua),
-            Self::Fs => fs::create(lua),
-            Self::Luau => luau::create(lua),
-            Self::Net => net::create(lua),
-            Self::Task => task::create(lua),
-            Self::Process => process::create(lua),
-            Self::Regex => regex::create(lua),
-            Self::Serde => serde::create(lua),
-            Self::Stdio => stdio::create(lua),
-            #[cfg(feature = "roblox")]
-            Self::Roblox => roblox::create(lua),
-        };
-        match res {
-            Ok(v) => v.into_lua_multi(lua),
-            Err(e) => Err(e.context(format!(
-                "Failed to create builtin library '{}'",
-                self.name()
-            ))),
-        }
-    }
-}
-
-impl FromStr for LuneBuiltin {
-    type Err = String;
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s.trim().to_ascii_lowercase().as_str() {
-            "datetime" => Ok(Self::DateTime),
-            "fs" => Ok(Self::Fs),
-            "luau" => Ok(Self::Luau),
-            "net" => Ok(Self::Net),
-            "task" => Ok(Self::Task),
-            "process" => Ok(Self::Process),
-            "regex" => Ok(Self::Regex),
-            "serde" => Ok(Self::Serde),
-            "stdio" => Ok(Self::Stdio),
-            #[cfg(feature = "roblox")]
-            "roblox" => Ok(Self::Roblox),
-            _ => Err(format!("Unknown builtin library '{s}'")),
-        }
-    }
-}
diff --git a/src/lune/builtins/serde/encode_decode.rs b/src/lune/builtins/serde/encode_decode.rs
deleted file mode 100644
index 8457a25..0000000
--- a/src/lune/builtins/serde/encode_decode.rs
+++ /dev/null
@@ -1,131 +0,0 @@
-use bstr::{BString, ByteSlice};
-use mlua::prelude::*;
-
-use serde_json::Value as JsonValue;
-use serde_yaml::Value as YamlValue;
-use toml::Value as TomlValue;
-
-const LUA_SERIALIZE_OPTIONS: LuaSerializeOptions = LuaSerializeOptions::new()
-    .set_array_metatable(false)
-    .serialize_none_to_null(false)
-    .serialize_unit_to_null(false);
-
-const LUA_DESERIALIZE_OPTIONS: LuaDeserializeOptions = LuaDeserializeOptions::new()
-    .sort_keys(true)
-    .deny_recursive_tables(false)
-    .deny_unsupported_types(true);
-
-#[derive(Debug, Clone, Copy)]
-pub enum EncodeDecodeFormat {
-    Json,
-    Yaml,
-    Toml,
-}
-
-impl<'lua> FromLua<'lua> for EncodeDecodeFormat {
-    fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
-        if let LuaValue::String(s) = &value {
-            match s.to_string_lossy().to_ascii_lowercase().trim() {
-                "json" => Ok(Self::Json),
-                "yaml" => Ok(Self::Yaml),
-                "toml" => Ok(Self::Toml),
-                kind => Err(LuaError::FromLuaConversionError {
-                    from: value.type_name(),
-                    to: "EncodeDecodeFormat",
-                    message: Some(format!(
-                        "Invalid format '{kind}', valid formats are:  json, yaml, toml"
-                    )),
-                }),
-            }
-        } else {
-            Err(LuaError::FromLuaConversionError {
-                from: value.type_name(),
-                to: "EncodeDecodeFormat",
-                message: None,
-            })
-        }
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-pub struct EncodeDecodeConfig {
-    pub format: EncodeDecodeFormat,
-    pub pretty: bool,
-}
-
-impl EncodeDecodeConfig {
-    pub fn serialize_to_string<'lua>(
-        self,
-        lua: &'lua Lua,
-        value: LuaValue<'lua>,
-    ) -> LuaResult<LuaString<'lua>> {
-        let bytes = match self.format {
-            EncodeDecodeFormat::Json => {
-                let serialized: JsonValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
-                if self.pretty {
-                    serde_json::to_vec_pretty(&serialized).into_lua_err()?
-                } else {
-                    serde_json::to_vec(&serialized).into_lua_err()?
-                }
-            }
-            EncodeDecodeFormat::Yaml => {
-                let serialized: YamlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
-                let mut writer = Vec::with_capacity(128);
-                serde_yaml::to_writer(&mut writer, &serialized).into_lua_err()?;
-                writer
-            }
-            EncodeDecodeFormat::Toml => {
-                let serialized: TomlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
-                let s = if self.pretty {
-                    toml::to_string_pretty(&serialized).into_lua_err()?
-                } else {
-                    toml::to_string(&serialized).into_lua_err()?
-                };
-                s.as_bytes().to_vec()
-            }
-        };
-        lua.create_string(bytes)
-    }
-
-    pub fn deserialize_from_string(self, lua: &Lua, string: BString) -> LuaResult<LuaValue> {
-        let bytes = string.as_bytes();
-        match self.format {
-            EncodeDecodeFormat::Json => {
-                let value: JsonValue = serde_json::from_slice(bytes).into_lua_err()?;
-                lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
-            }
-            EncodeDecodeFormat::Yaml => {
-                let value: YamlValue = serde_yaml::from_slice(bytes).into_lua_err()?;
-                lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
-            }
-            EncodeDecodeFormat::Toml => {
-                if let Ok(s) = string.to_str() {
-                    let value: TomlValue = toml::from_str(s).into_lua_err()?;
-                    lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
-                } else {
-                    Err(LuaError::RuntimeError(
-                        "TOML must be valid utf-8".to_string(),
-                    ))
-                }
-            }
-        }
-    }
-}
-
-impl From<EncodeDecodeFormat> for EncodeDecodeConfig {
-    fn from(format: EncodeDecodeFormat) -> Self {
-        Self {
-            format,
-            pretty: false,
-        }
-    }
-}
-
-impl From<(EncodeDecodeFormat, bool)> for EncodeDecodeConfig {
-    fn from(value: (EncodeDecodeFormat, bool)) -> Self {
-        Self {
-            format: value.0,
-            pretty: value.1,
-        }
-    }
-}
diff --git a/src/lune/builtins/serde/mod.rs b/src/lune/builtins/serde/mod.rs
deleted file mode 100644
index de351a3..0000000
--- a/src/lune/builtins/serde/mod.rs
+++ /dev/null
@@ -1,48 +0,0 @@
-use bstr::BString;
-use mlua::prelude::*;
-
-pub(super) mod compress_decompress;
-pub(super) mod encode_decode;
-
-use compress_decompress::{compress, decompress, CompressDecompressFormat};
-use encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat};
-
-use crate::lune::util::TableBuilder;
-
-pub fn create(lua: &Lua) -> LuaResult<LuaTable> {
-    TableBuilder::new(lua)?
-        .with_function("encode", serde_encode)?
-        .with_function("decode", serde_decode)?
-        .with_async_function("compress", serde_compress)?
-        .with_async_function("decompress", serde_decompress)?
-        .build_readonly()
-}
-
-fn serde_encode<'lua>(
-    lua: &'lua Lua,
-    (format, val, pretty): (EncodeDecodeFormat, LuaValue<'lua>, Option<bool>),
-) -> LuaResult<LuaString<'lua>> {
-    let config = EncodeDecodeConfig::from((format, pretty.unwrap_or_default()));
-    config.serialize_to_string(lua, val)
-}
-
-fn serde_decode(lua: &Lua, (format, str): (EncodeDecodeFormat, BString)) -> LuaResult<LuaValue> {
-    let config = EncodeDecodeConfig::from(format);
-    config.deserialize_from_string(lua, str)
-}
-
-async fn serde_compress(
-    lua: &Lua,
-    (format, str): (CompressDecompressFormat, BString),
-) -> LuaResult<LuaString> {
-    let bytes = compress(format, str).await?;
-    lua.create_string(bytes)
-}
-
-async fn serde_decompress(
-    lua: &Lua,
-    (format, str): (CompressDecompressFormat, BString),
-) -> LuaResult<LuaString> {
-    let bytes = decompress(format, str).await?;
-    lua.create_string(bytes)
-}
diff --git a/src/lune/builtins/stdio/mod.rs b/src/lune/builtins/stdio/mod.rs
deleted file mode 100644
index f851bc7..0000000
--- a/src/lune/builtins/stdio/mod.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-use mlua::prelude::*;
-
-use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select};
-use mlua_luau_scheduler::LuaSpawnExt;
-use tokio::io::{self, AsyncReadExt, AsyncWriteExt};
-
-use crate::lune::util::{
-    formatting::{
-        format_style, pretty_format_multi_value, style_from_color_str, style_from_style_str,
-    },
-    TableBuilder,
-};
-
-mod prompt;
-use prompt::{PromptKind, PromptOptions, PromptResult};
-
-pub fn create(lua: &Lua) -> LuaResult<LuaTable<'_>> {
-    TableBuilder::new(lua)?
-        .with_function("color", stdio_color)?
-        .with_function("style", stdio_style)?
-        .with_function("format", stdio_format)?
-        .with_async_function("write", stdio_write)?
-        .with_async_function("ewrite", stdio_ewrite)?
-        .with_async_function("readToEnd", stdio_read_to_end)?
-        .with_async_function("prompt", stdio_prompt)?
-        .build_readonly()
-}
-
-fn stdio_color(_: &Lua, color: String) -> LuaResult<String> {
-    let ansi_string = format_style(style_from_color_str(&color)?);
-    Ok(ansi_string)
-}
-
-fn stdio_style(_: &Lua, color: String) -> LuaResult<String> {
-    let ansi_string = format_style(style_from_style_str(&color)?);
-    Ok(ansi_string)
-}
-
-fn stdio_format(_: &Lua, args: LuaMultiValue) -> LuaResult<String> {
-    pretty_format_multi_value(&args)
-}
-
-async fn stdio_write(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
-    let mut stdout = io::stdout();
-    stdout.write_all(s.as_bytes()).await?;
-    stdout.flush().await?;
-    Ok(())
-}
-
-async fn stdio_ewrite(_: &Lua, s: LuaString<'_>) -> LuaResult<()> {
-    let mut stderr = io::stderr();
-    stderr.write_all(s.as_bytes()).await?;
-    stderr.flush().await?;
-    Ok(())
-}
-
-/*
-    FUTURE: Figure out how to expose some kind of "readLine" function using a buffered reader.
-
-    This is a bit tricky since we would want to be able to use **both** readLine and readToEnd
-    in the same script, doing something like readLine, readLine, readToEnd from lua, and
-    having that capture the first two lines and then read the rest of the input.
-*/
-
-async fn stdio_read_to_end(lua: &Lua, _: ()) -> LuaResult<LuaString> {
-    let mut input = Vec::new();
-    let mut stdin = io::stdin();
-    stdin.read_to_end(&mut input).await?;
-    lua.create_string(&input)
-}
-
-async fn stdio_prompt(lua: &Lua, options: PromptOptions) -> LuaResult<PromptResult> {
-    lua.spawn_blocking(move || prompt(options))
-        .await
-        .into_lua_err()
-}
-
-fn prompt(options: PromptOptions) -> LuaResult<PromptResult> {
-    let theme = ColorfulTheme::default();
-    match options.kind {
-        PromptKind::Text => {
-            let input: String = Input::with_theme(&theme)
-                .allow_empty(true)
-                .with_prompt(options.text.unwrap_or_default())
-                .with_initial_text(options.default_string.unwrap_or_default())
-                .interact_text()
-                .into_lua_err()?;
-            Ok(PromptResult::String(input))
-        }
-        PromptKind::Confirm => {
-            let mut prompt = Confirm::with_theme(&theme);
-            if let Some(b) = options.default_bool {
-                prompt = prompt.default(b);
-            };
-            let result = prompt
-                .with_prompt(&options.text.expect("Missing text in prompt options"))
-                .interact()
-                .into_lua_err()?;
-            Ok(PromptResult::Boolean(result))
-        }
-        PromptKind::Select => {
-            let chosen = Select::with_theme(&theme)
-                .with_prompt(&options.text.unwrap_or_default())
-                .items(&options.options.expect("Missing options in prompt options"))
-                .interact_opt()
-                .into_lua_err()?;
-            Ok(match chosen {
-                Some(idx) => PromptResult::Index(idx + 1),
-                None => PromptResult::None,
-            })
-        }
-        PromptKind::MultiSelect => {
-            let chosen = MultiSelect::with_theme(&theme)
-                .with_prompt(&options.text.unwrap_or_default())
-                .items(&options.options.expect("Missing options in prompt options"))
-                .interact_opt()
-                .into_lua_err()?;
-            Ok(match chosen {
-                None => PromptResult::None,
-                Some(indices) => {
-                    PromptResult::Indices(indices.iter().map(|idx| *idx + 1).collect())
-                }
-            })
-        }
-    }
-}
diff --git a/src/lune/globals/g_table.rs b/src/lune/globals/g_table.rs
deleted file mode 100644
index 8c007c8..0000000
--- a/src/lune/globals/g_table.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-use mlua::prelude::*;
-
-pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
-    lua.create_table()
-}
diff --git a/src/lune/globals/mod.rs b/src/lune/globals/mod.rs
deleted file mode 100644
index 1d8700c..0000000
--- a/src/lune/globals/mod.rs
+++ /dev/null
@@ -1,26 +0,0 @@
-use mlua::prelude::*;
-
-use super::util::TableBuilder;
-
-mod g_table;
-mod print;
-mod require;
-mod version;
-mod warn;
-
-pub fn inject_all(lua: &Lua) -> LuaResult<()> {
-    let all = TableBuilder::new(lua)?
-        .with_value("_G", g_table::create(lua)?)?
-        .with_value("_VERSION", version::create(lua)?)?
-        .with_value("print", print::create(lua)?)?
-        .with_value("require", require::create(lua)?)?
-        .with_value("warn", warn::create(lua)?)?
-        .build_readonly()?;
-
-    for res in all.pairs() {
-        let (key, value): (LuaValue, LuaValue) = res.unwrap();
-        lua.globals().set(key, value)?;
-    }
-
-    Ok(())
-}
diff --git a/src/lune/globals/print.rs b/src/lune/globals/print.rs
deleted file mode 100644
index 944d420..0000000
--- a/src/lune/globals/print.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use std::io::Write as _;
-
-use mlua::prelude::*;
-
-use crate::lune::util::formatting::pretty_format_multi_value;
-
-pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
-    lua.create_function(|_, args: LuaMultiValue| {
-        let formatted = format!("{}\n", pretty_format_multi_value(&args)?);
-        let mut stdout = std::io::stdout();
-        stdout.write_all(formatted.as_bytes())?;
-        stdout.flush()?;
-        Ok(())
-    })
-}
diff --git a/src/lune/globals/version.rs b/src/lune/globals/version.rs
deleted file mode 100644
index 1228fd0..0000000
--- a/src/lune/globals/version.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-use mlua::prelude::*;
-
-pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
-    let lune_version = format!("Lune {}", env!("CARGO_PKG_VERSION"));
-
-    let luau_version_full = lua
-        .globals()
-        .get::<_, LuaString>("_VERSION")
-        .expect("Missing _VERSION global");
-    let luau_version_str = luau_version_full
-        .to_str()
-        .context("Invalid utf8 found in _VERSION global")?;
-
-    // If this function runs more than once, we
-    // may get an already formatted lune version.
-    if luau_version_str.starts_with(lune_version.as_str()) {
-        return Ok(luau_version_full);
-    }
-
-    // Luau version is expected to be in the format "Luau 0.x" and sometimes "Luau 0.x.y"
-    if !luau_version_str.starts_with("Luau 0.") {
-        panic!("_VERSION global is formatted incorrectly\nGot: '{luau_version_str}'")
-    }
-    let luau_version = luau_version_str.strip_prefix("Luau 0.").unwrap().trim();
-
-    // We make some guarantees about the format of the _VERSION global,
-    // so make sure that the luau version also follows those rules.
-    if luau_version.is_empty() {
-        panic!("_VERSION global is missing version number\nGot: '{luau_version_str}'")
-    } else if !luau_version.chars().all(is_valid_version_char) {
-        panic!("_VERSION global contains invalid characters\nGot: '{luau_version_str}'")
-    }
-
-    lua.create_string(format!("{lune_version}+{luau_version}"))
-}
-
-fn is_valid_version_char(c: char) -> bool {
-    matches!(c, '0'..='9' | '.')
-}
diff --git a/src/lune/globals/warn.rs b/src/lune/globals/warn.rs
deleted file mode 100644
index dfc409e..0000000
--- a/src/lune/globals/warn.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-use std::io::Write as _;
-
-use mlua::prelude::*;
-
-use crate::lune::util::formatting::{format_label, pretty_format_multi_value};
-
-pub fn create(lua: &Lua) -> LuaResult<impl IntoLua<'_>> {
-    lua.create_function(|_, args: LuaMultiValue| {
-        let formatted = format!(
-            "{}\n{}\n",
-            format_label("warn"),
-            pretty_format_multi_value(&args)?
-        );
-        let mut stderr = std::io::stderr();
-        stderr.write_all(formatted.as_bytes())?;
-        stderr.flush()?;
-        Ok(())
-    })
-}
diff --git a/src/lune/util/formatting.rs b/src/lune/util/formatting.rs
deleted file mode 100644
index 54376b7..0000000
--- a/src/lune/util/formatting.rs
+++ /dev/null
@@ -1,477 +0,0 @@
-use std::fmt::Write;
-
-use console::{colors_enabled, set_colors_enabled, style, Style};
-use mlua::prelude::*;
-use once_cell::sync::Lazy;
-
-const MAX_FORMAT_DEPTH: usize = 4;
-
-const INDENT: &str = "    ";
-
-pub const STYLE_RESET_STR: &str = "\x1b[0m";
-
-// Colors
-pub static COLOR_BLACK: Lazy<Style> = Lazy::new(|| Style::new().black());
-pub static COLOR_RED: Lazy<Style> = Lazy::new(|| Style::new().red());
-pub static COLOR_GREEN: Lazy<Style> = Lazy::new(|| Style::new().green());
-pub static COLOR_YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow());
-pub static COLOR_BLUE: Lazy<Style> = Lazy::new(|| Style::new().blue());
-pub static COLOR_PURPLE: Lazy<Style> = Lazy::new(|| Style::new().magenta());
-pub static COLOR_CYAN: Lazy<Style> = Lazy::new(|| Style::new().cyan());
-pub static COLOR_WHITE: Lazy<Style> = Lazy::new(|| Style::new().white());
-
-// Styles
-pub static STYLE_BOLD: Lazy<Style> = Lazy::new(|| Style::new().bold());
-pub static STYLE_DIM: Lazy<Style> = Lazy::new(|| Style::new().dim());
-
-fn can_be_plain_lua_table_key(s: &LuaString) -> bool {
-    let str = s.to_string_lossy().to_string();
-    let first_char = str.chars().next().unwrap();
-    if first_char.is_alphabetic() {
-        str.chars().all(|c| c == '_' || c.is_alphanumeric())
-    } else {
-        false
-    }
-}
-
-pub fn format_label<S: AsRef<str>>(s: S) -> String {
-    format!(
-        "{}{}{} ",
-        style("[").dim(),
-        match s.as_ref().to_ascii_lowercase().as_str() {
-            "info" => style("INFO").blue(),
-            "warn" => style("WARN").yellow(),
-            "error" => style("ERROR").red(),
-            _ => style(""),
-        },
-        style("]").dim()
-    )
-}
-
-pub fn format_style(style: Option<&'static Style>) -> String {
-    if cfg!(test) {
-        "".to_string()
-    } else if let Some(style) = style {
-        // HACK: We have no direct way of referencing the ansi color code
-        // of the style that console::Style provides, and we also know for
-        // sure that styles always include the reset sequence at the end,
-        // unless we are in a CI environment on non-interactive terminal
-        style
-            .apply_to("")
-            .to_string()
-            .trim_end_matches(STYLE_RESET_STR)
-            .to_string()
-    } else {
-        STYLE_RESET_STR.to_string()
-    }
-}
-
-pub fn style_from_color_str<S: AsRef<str>>(s: S) -> LuaResult<Option<&'static Style>> {
-    Ok(match s.as_ref() {
-        "reset" => None,
-        "black" => Some(&COLOR_BLACK),
-        "red" => Some(&COLOR_RED),
-        "green" => Some(&COLOR_GREEN),
-        "yellow" => Some(&COLOR_YELLOW),
-        "blue" => Some(&COLOR_BLUE),
-        "purple" => Some(&COLOR_PURPLE),
-        "cyan" => Some(&COLOR_CYAN),
-        "white" => Some(&COLOR_WHITE),
-        _ => {
-            return Err(LuaError::RuntimeError(format!(
-                "The color '{}' is not a valid color name",
-                s.as_ref()
-            )));
-        }
-    })
-}
-
-pub fn style_from_style_str<S: AsRef<str>>(s: S) -> LuaResult<Option<&'static Style>> {
-    Ok(match s.as_ref() {
-        "reset" => None,
-        "bold" => Some(&STYLE_BOLD),
-        "dim" => Some(&STYLE_DIM),
-        _ => {
-            return Err(LuaError::RuntimeError(format!(
-                "The style '{}' is not a valid style name",
-                s.as_ref()
-            )));
-        }
-    })
-}
-
-pub fn pretty_format_value(
-    buffer: &mut String,
-    value: &LuaValue,
-    parent_table_addr: Option<String>,
-    depth: usize,
-) -> std::fmt::Result {
-    match &value {
-        LuaValue::Nil => write!(buffer, "nil")?,
-        LuaValue::Boolean(true) => write!(buffer, "{}", COLOR_YELLOW.apply_to("true"))?,
-        LuaValue::Boolean(false) => write!(buffer, "{}", COLOR_YELLOW.apply_to("false"))?,
-        LuaValue::Number(n) => write!(buffer, "{}", COLOR_CYAN.apply_to(format!("{n}")))?,
-        LuaValue::Integer(i) => write!(buffer, "{}", COLOR_CYAN.apply_to(format!("{i}")))?,
-        LuaValue::String(s) => write!(
-            buffer,
-            "\"{}\"",
-            COLOR_GREEN.apply_to(
-                s.to_string_lossy()
-                    .replace('"', r#"\""#)
-                    .replace('\r', r"\r")
-                    .replace('\n', r"\n")
-            )
-        )?,
-        LuaValue::Table(ref tab) => {
-            let table_addr = Some(format!("{:p}", tab.to_pointer()));
-
-            if depth >= MAX_FORMAT_DEPTH {
-                write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?;
-            } else if let Some(s) = call_table_tostring_metamethod(tab) {
-                write!(buffer, "{s}")?;
-            } else if depth >= 1 && parent_table_addr.eq(&table_addr) {
-                write!(buffer, "{}", STYLE_DIM.apply_to("<self>"))?;
-            } else {
-                let mut is_empty = false;
-                let depth_indent = INDENT.repeat(depth);
-                write!(buffer, "{}", STYLE_DIM.apply_to("{"))?;
-                for pair in tab.clone().pairs::<LuaValue, LuaValue>() {
-                    let (key, value) = pair.unwrap();
-                    match &key {
-                        LuaValue::String(s) if can_be_plain_lua_table_key(s) => write!(
-                            buffer,
-                            "\n{}{}{} {} ",
-                            depth_indent,
-                            INDENT,
-                            s.to_string_lossy(),
-                            STYLE_DIM.apply_to("=")
-                        )?,
-                        _ => {
-                            write!(buffer, "\n{depth_indent}{INDENT}[")?;
-                            pretty_format_value(
-                                buffer,
-                                &key,
-                                parent_table_addr.clone(),
-                                depth + 1,
-                            )?;
-                            write!(buffer, "] {} ", STYLE_DIM.apply_to("="))?;
-                        }
-                    }
-                    pretty_format_value(buffer, &value, parent_table_addr.clone(), depth + 1)?;
-                    write!(buffer, "{}", STYLE_DIM.apply_to(","))?;
-                    is_empty = false;
-                }
-                if is_empty {
-                    write!(buffer, "{}", STYLE_DIM.apply_to(" }"))?;
-                } else {
-                    write!(buffer, "\n{depth_indent}{}", STYLE_DIM.apply_to("}"))?;
-                }
-            }
-        }
-        LuaValue::Vector(v) => write!(
-            buffer,
-            "{}",
-            COLOR_PURPLE.apply_to(format!(
-                "<vector({x}, {y}, {z})>",
-                x = v.x(),
-                y = v.y(),
-                z = v.z()
-            ))
-        )?,
-        LuaValue::Thread(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<thread>"))?,
-        LuaValue::Function(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<function>"))?,
-        LuaValue::UserData(u) => {
-            if let Some(s) = call_userdata_tostring_metamethod(u) {
-                write!(buffer, "{s}")?
-            } else {
-                write!(buffer, "{}", COLOR_PURPLE.apply_to("<userdata>"))?
-            }
-        }
-        LuaValue::LightUserData(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<userdata>"))?,
-        LuaValue::Error(e) => write!(buffer, "{}", pretty_format_luau_error(e, false),)?,
-    }
-    Ok(())
-}
-
-pub fn pretty_format_multi_value(multi: &LuaMultiValue) -> LuaResult<String> {
-    let mut buffer = String::new();
-    let mut counter = 0;
-    for value in multi {
-        counter += 1;
-        if let LuaValue::String(s) = value {
-            write!(buffer, "{}", s.to_string_lossy()).into_lua_err()?;
-        } else {
-            let addr = format!("{:p}", value.to_pointer());
-
-            pretty_format_value(&mut buffer, value, Some(addr), 0).into_lua_err()?;
-        }
-        if counter < multi.len() {
-            write!(&mut buffer, " ").into_lua_err()?;
-        }
-    }
-    Ok(buffer)
-}
-
-pub fn pretty_format_luau_error(e: &LuaError, colorized: bool) -> String {
-    let previous_colors_enabled = if !colorized {
-        set_colors_enabled(false);
-        Some(colors_enabled())
-    } else {
-        None
-    };
-    let stack_begin = format!("[{}]", COLOR_BLUE.apply_to("Stack Begin"));
-    let stack_end = format!("[{}]", COLOR_BLUE.apply_to("Stack End"));
-    let err_string = match e {
-        LuaError::RuntimeError(e) => {
-            // Remove unnecessary prefix
-            let mut err_string = e.to_string();
-            if let Some(no_prefix) = err_string.strip_prefix("runtime error: ") {
-                err_string = no_prefix.to_string();
-            }
-            // Add "Stack Begin" instead of default stack traceback string
-            let mut err_lines = err_string
-                .lines()
-                .map(|s| s.to_string())
-                .collect::<Vec<String>>();
-            let mut found_stack_begin = false;
-            for (index, line) in err_lines.clone().iter().enumerate().rev() {
-                if *line == "stack traceback:" {
-                    err_lines[index] = stack_begin.clone();
-                    found_stack_begin = true;
-                    break;
-                }
-            }
-            // Add "Stack End" to the very end of the stack trace for symmetry
-            if found_stack_begin {
-                err_lines.push(stack_end.clone());
-            }
-            err_lines.join("\n")
-        }
-        LuaError::CallbackError { traceback, cause } => {
-            // Find the best traceback (most lines) and the root error message
-            // The traceback may also start with "override traceback:" which
-            // means it was passed from somewhere that wants a custom trace,
-            // so we should then respect that and get the best override instead
-            let mut full_trace = traceback.to_string();
-            let mut root_cause = cause.as_ref();
-            let mut trace_override = false;
-            while let LuaError::CallbackError { cause, traceback } = root_cause {
-                let is_override = traceback.starts_with("override traceback:");
-                if is_override {
-                    if !trace_override || traceback.lines().count() > full_trace.len() {
-                        full_trace = traceback
-                            .trim_start_matches("override traceback:")
-                            .to_string();
-                        trace_override = true;
-                    }
-                } else if !trace_override {
-                    full_trace = format!("{traceback}\n{full_trace}");
-                }
-                root_cause = cause;
-            }
-            // If we got a runtime error with an embedded traceback, we should
-            // use that instead since it generally contains more information
-            if matches!(root_cause, LuaError::RuntimeError(e) if e.contains("stack traceback:")) {
-                pretty_format_luau_error(root_cause, colorized)
-            } else {
-                // Otherwise we format whatever root error we got using
-                // the same error formatting as for above runtime errors
-                format!(
-                    "{}\n{}\n{}\n{}",
-                    pretty_format_luau_error(root_cause, colorized),
-                    stack_begin,
-                    full_trace.trim_start_matches("stack traceback:\n"),
-                    stack_end
-                )
-            }
-        }
-        LuaError::BadArgument { pos, cause, .. } => match cause.as_ref() {
-            // TODO: Add more detail to this error message
-            LuaError::FromLuaConversionError { from, to, .. } => {
-                format!("Argument #{pos} must be of type '{to}', got '{from}'")
-            }
-            c => format!(
-                "Bad argument #{pos}\n{}",
-                pretty_format_luau_error(c, colorized)
-            ),
-        },
-        e => format!("{e}"),
-    };
-    // Re-enable colors if they were previously enabled
-    if let Some(true) = previous_colors_enabled {
-        set_colors_enabled(true)
-    }
-    // Remove the script path from the error message
-    // itself, it can be found in the stack trace
-    let mut err_lines = err_string.lines().collect::<Vec<_>>();
-    if let Some(first_line) = err_lines.first() {
-        if first_line.starts_with("[string \"") {
-            if let Some(closing_bracket) = first_line.find("]:") {
-                let after_closing_bracket = &first_line[closing_bracket + 2..first_line.len()];
-                if let Some(last_colon) = after_closing_bracket.find(": ") {
-                    err_lines[0] = &after_closing_bracket
-                        [last_colon + 2..first_line.len() - closing_bracket - 2];
-                } else {
-                    err_lines[0] = after_closing_bracket
-                }
-            }
-        }
-    }
-    // Find where the stack trace stars and ends
-    let stack_begin_idx =
-        err_lines.iter().enumerate().find_map(
-            |(i, line)| {
-                if *line == stack_begin {
-                    Some(i)
-                } else {
-                    None
-                }
-            },
-        );
-    let stack_end_idx =
-        err_lines.iter().enumerate().find_map(
-            |(i, line)| {
-                if *line == stack_end {
-                    Some(i)
-                } else {
-                    None
-                }
-            },
-        );
-    // If we have a stack trace, we should transform the formatting from the
-    // default mlua formatting into something more friendly, similar to Roblox
-    if let (Some(idx_start), Some(idx_end)) = (stack_begin_idx, stack_end_idx) {
-        let stack_lines = err_lines
-            .iter()
-            .enumerate()
-            // Filter out stack lines
-            .filter_map(|(idx, line)| {
-                if idx > idx_start && idx < idx_end {
-                    Some(*line)
-                } else {
-                    None
-                }
-            })
-            // Transform from mlua format into friendly format, while also
-            // ensuring that leading whitespace / indentation is consistent
-            .map(transform_stack_line)
-            .collect::<Vec<_>>();
-        fix_error_nitpicks(format!(
-            "{}\n{}\n{}\n{}",
-            err_lines
-                .iter()
-                .take(idx_start)
-                .copied()
-                .collect::<Vec<_>>()
-                .join("\n"),
-            stack_begin,
-            stack_lines.join("\n"),
-            stack_end,
-        ))
-    } else {
-        fix_error_nitpicks(err_string)
-    }
-}
-
-fn transform_stack_line(line: &str) -> String {
-    match (line.find('['), line.find(']')) {
-        (Some(idx_start), Some(idx_end)) => {
-            let name = line[idx_start..idx_end + 1]
-                .trim_start_matches('[')
-                .trim_start_matches("string ")
-                .trim_start_matches('"')
-                .trim_end_matches(']')
-                .trim_end_matches('"');
-            let after_name = &line[idx_end + 1..];
-            let line_num = match after_name.find(':') {
-                Some(lineno_start) => match after_name[lineno_start + 1..].find(':') {
-                    Some(lineno_end) => &after_name[lineno_start + 1..lineno_end + 1],
-                    None => match after_name.contains("in function") || after_name.contains("in ?")
-                    {
-                        false => &after_name[lineno_start + 1..],
-                        true => "",
-                    },
-                },
-                None => "",
-            };
-            let func_name = match after_name.find("in function ") {
-                Some(func_start) => after_name[func_start + 12..]
-                    .trim()
-                    .trim_end_matches('\'')
-                    .trim_start_matches('\'')
-                    .trim_start_matches("_G."),
-                None => "",
-            };
-            let mut result = String::new();
-            write!(
-                result,
-                "    Script '{}'",
-                match name {
-                    "C" => "[C]",
-                    name => name,
-                },
-            )
-            .unwrap();
-            if !line_num.is_empty() {
-                write!(result, ", Line {line_num}").unwrap();
-            }
-            if !func_name.is_empty() {
-                write!(result, " - function {func_name}").unwrap();
-            }
-            result
-        }
-        (_, _) => line.to_string(),
-    }
-}
-
-fn fix_error_nitpicks(full_message: String) -> String {
-    full_message
-        // Hacky fix for our custom require appearing as a normal script
-        // TODO: It's probably better to pull in the regex crate here ..
-        .replace("'require', Line 5", "'[C]' - function require")
-        .replace("'require', Line 7", "'[C]' - function require")
-        .replace("'require', Line 8", "'[C]' - function require")
-        // Same thing here for our async script
-        .replace("'async', Line 2", "'[C]'")
-        .replace("'async', Line 3", "'[C]'")
-        // Fix error calls in custom script chunks coming through
-        .replace(
-            "'[C]' - function error\n    Script '[C]' - function require",
-            "'[C]' - function require",
-        )
-        // Fix strange double require
-        .replace(
-            "'[C]' - function require - function require",
-            "'[C]' - function require",
-        )
-        // Fix strange double C
-        .replace("'[C]'\n    Script '[C]'", "'[C]'")
-}
-
-fn call_table_tostring_metamethod<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
-    let f = match tab.get_metatable() {
-        None => None,
-        Some(meta) => match meta.get::<_, LuaFunction>(LuaMetaMethod::ToString.name()) {
-            Ok(method) => Some(method),
-            Err(_) => None,
-        },
-    }?;
-    match f.call::<_, String>(()) {
-        Ok(res) => Some(res),
-        Err(_) => None,
-    }
-}
-
-fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
-    let f = match tab.get_metatable() {
-        Err(_) => None,
-        Ok(meta) => match meta.get::<LuaFunction>(LuaMetaMethod::ToString.name()) {
-            Ok(method) => Some(method),
-            Err(_) => None,
-        },
-    }?;
-    match f.call::<_, String>(()) {
-        Ok(res) => Some(res),
-        Err(_) => None,
-    }
-}
diff --git a/src/lune/util/mod.rs b/src/lune/util/mod.rs
deleted file mode 100644
index 1683428..0000000
--- a/src/lune/util/mod.rs
+++ /dev/null
@@ -1,8 +0,0 @@
-mod table_builder;
-
-pub mod formatting;
-pub mod luaurc;
-pub mod paths;
-pub mod traits;
-
-pub use table_builder::TableBuilder;
diff --git a/src/lune/util/paths.rs b/src/lune/util/paths.rs
deleted file mode 100644
index c439858..0000000
--- a/src/lune/util/paths.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-use std::{
-    env::current_dir,
-    path::{Path, PathBuf},
-};
-
-use once_cell::sync::Lazy;
-use path_clean::PathClean;
-
-pub static CWD: Lazy<PathBuf> = Lazy::new(|| {
-    let cwd = current_dir().expect("failed to find current working directory");
-    dunce::canonicalize(cwd).expect("failed to canonicalize current working directory")
-});
-
-pub fn make_absolute_and_clean(path: impl AsRef<Path>) -> PathBuf {
-    let path = path.as_ref();
-    if path.is_relative() {
-        CWD.join(path).clean()
-    } else {
-        path.clean()
-    }
-}
diff --git a/src/lune/util/traits.rs b/src/lune/util/traits.rs
deleted file mode 100644
index 2c75d65..0000000
--- a/src/lune/util/traits.rs
+++ /dev/null
@@ -1,15 +0,0 @@
-use mlua::prelude::*;
-
-use super::formatting::format_label;
-use crate::RuntimeError;
-
-pub trait LuaEmitErrorExt {
-    fn emit_error(&self, err: LuaError);
-}
-
-impl LuaEmitErrorExt for Lua {
-    fn emit_error(&self, err: LuaError) {
-        // NOTE: LuneError will pretty-format this error
-        eprintln!("{}\n{}", format_label("error"), RuntimeError::from(err));
-    }
-}
diff --git a/src/main.rs b/src/main.rs
deleted file mode 100644
index 734436d..0000000
--- a/src/main.rs
+++ /dev/null
@@ -1,47 +0,0 @@
-#![deny(clippy::all)]
-#![warn(clippy::cargo, clippy::pedantic)]
-#![allow(
-    clippy::cargo_common_metadata,
-    clippy::match_bool,
-    clippy::module_name_repetitions,
-    clippy::multiple_crate_versions,
-    clippy::needless_pass_by_value,
-    clippy::declare_interior_mutable_const,
-    clippy::borrow_interior_mutable_const
-)]
-
-use std::process::ExitCode;
-
-pub(crate) mod cli;
-pub(crate) mod standalone;
-
-use cli::Cli;
-use console::style;
-
-#[tokio::main(flavor = "multi_thread")]
-async fn main() -> ExitCode {
-    tracing_subscriber::fmt()
-        .compact()
-        .with_env_filter(tracing_subscriber::filter::EnvFilter::from_default_env())
-        .with_target(true)
-        .with_timer(tracing_subscriber::fmt::time::uptime())
-        .with_level(true)
-        .init();
-
-    if let Some(bin) = standalone::check().await {
-        return standalone::run(bin).await.unwrap();
-    }
-
-    match Cli::new().run().await {
-        Ok(code) => code,
-        Err(err) => {
-            eprintln!(
-                "{}{}{}\n{err:?}",
-                style("[").dim(),
-                style("ERROR").red(),
-                style("]").dim(),
-            );
-            ExitCode::FAILURE
-        }
-    }
-}
diff --git a/tests/net/serve/requests.luau b/tests/net/serve/requests.luau
index 275f356..36602a0 100644
--- a/tests/net/serve/requests.luau
+++ b/tests/net/serve/requests.luau
@@ -3,7 +3,7 @@ local process = require("@lune/process")
 local stdio = require("@lune/stdio")
 local task = require("@lune/task")
 
-local PORT = 8080
+local PORT = 8082
 local URL = `http://127.0.0.1:{PORT}`
 local URL_EXTERNAL = `http://0.0.0.0`
 local RESPONSE = "Hello, lune!"
diff --git a/tests/require/tests/state_module.luau b/tests/require/tests/state_module.luau
index 9b6135e..156f5bc 100644
--- a/tests/require/tests/state_module.luau
+++ b/tests/require/tests/state_module.luau
@@ -3,7 +3,7 @@ local M = {}
 M.state = 10
 
 function M.set_state(n: number)
-    M.state = n
+	M.state = n
 end
 
 return M
diff --git a/tests/serde/test-files/loremipsum.txt.gz b/tests/serde/test-files/loremipsum.txt.gz
index 5d1cbf3ab4b5b10f4c49820bd2bc71d229fe79d8..206a5e81cc1a50dc11fef1ba3d00291544d887aa 100644
GIT binary patch
delta 118
zcmV-+0Ez#M0gM5V9dFzsOG%u@_Wpo`SONw(k6jl#WLk7oZMtCM$eo^0bQp`%V<A_W
zVnxOSY~ma4tYC<BtqGQ=a3CB!Pbu;yoPFv_7|~k(xDPov>MO;imLLOXjv_}G8WmUM
Y{_SJN3fpe`|L_M6pFm6zrHBFm00}`eg8%>k

delta 118
zcmV-+0Ez#M0gM5V9dEekN=Y1Idw)PeEI|i2k6o8^a9VU!ZMtA|<Sw32beI$ukA+-0
zX@%nfY4i<usgR&`tqGPVWgr}Ko-F(jE`91s7|~k(x(^;Q>N_P%ErA1Pj>01hO%%__
Y{ny8g6}H{>>+lZ_UqDO|rHBFm04rfJ761SM

diff --git a/tests/serde/test-files/loremipsum.txt.lz4 b/tests/serde/test-files/loremipsum.txt.lz4
index 9d8d46f9908bd41fc2246330a577a9e0289dee3d..801bf9009893f7b9cc25f34b9b6236c1afb199f2 100644
GIT binary patch
delta 48
zcmZ3*xSLV1gNcEGMafqp#bNmp1_p)??i1O<n2i`NPfT#)KOaz3TCB$f6lPGle(fay
DJ`fI8

delta 31
mcmdnZxQdajgNcFRbHqf!FqV?iB87=rP8{bFN{dR1^|%0?tqL>%

diff --git a/tests/serde/test-files/loremipsum.txt.z b/tests/serde/test-files/loremipsum.txt.z
index 9f85f5a..9830fd8 100644
--- a/tests/serde/test-files/loremipsum.txt.z
+++ b/tests/serde/test-files/loremipsum.txt.z
@@ -1 +1,2 @@
-x���
�0E���u��1A���JIb{@B,A8�]�tpZtTm�`t�.�Pt���\9i��i�
w���u�	�'e r�,�.}�J�Z��2�;%K-�g��#M����nj�����_���
\ No newline at end of file
+x���
�0E���u��1A��"KI������X8�];dZtTm�`�ȝ�Pt��c!W�bE�l��`�u�	��p <O)�&�}�J�Z��98�+ť� g�"GW���c�
+�n�������
\ No newline at end of file

From efcc3c60286f8241e7fb18b0bfa028eca2adc836 Mon Sep 17 00:00:00 2001
From: Filip Tibell <filip.tibell@gmail.com>
Date: Sun, 12 May 2024 13:52:02 +0200
Subject: [PATCH 2/7] Update dependencies

---
 Cargo.lock | 293 +++++++++++++++++++++++++++++------------------------
 1 file changed, 163 insertions(+), 130 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index b4455c2..a4cfe3a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -69,47 +69,48 @@ dependencies = [
 
 [[package]]
 name = "anstream"
-version = "0.6.13"
+version = "0.6.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
 dependencies = [
  "anstyle",
  "anstyle-parse",
  "anstyle-query",
  "anstyle-wincon",
  "colorchoice",
+ "is_terminal_polyfill",
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle"
-version = "1.0.6"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
 dependencies = [
  "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.2"
+version = "3.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
 dependencies = [
  "anstyle",
  "windows-sys 0.52.0",
@@ -117,9 +118,9 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.82"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
+checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
 
 [[package]]
 name = "arbitrary"
@@ -156,7 +157,7 @@ checksum = "136d4d23bcc79e27423727b36823d86233aad06dfea531837b038394d11e9928"
 dependencies = [
  "concurrent-queue",
  "event-listener 5.3.0",
- "event-listener-strategy 0.5.1",
+ "event-listener-strategy 0.5.2",
  "futures-core",
  "pin-project-lite",
 ]
@@ -201,9 +202,9 @@ dependencies = [
 
 [[package]]
 name = "async-task"
-version = "4.7.0"
+version = "4.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
 
 [[package]]
 name = "atomic-waker"
@@ -213,9 +214,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
 
 [[package]]
 name = "autocfg"
-version = "1.2.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
 
 [[package]]
 name = "backtrace"
@@ -297,18 +298,16 @@ dependencies = [
 
 [[package]]
 name = "blocking"
-version = "1.5.1"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
+checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88"
 dependencies = [
  "async-channel",
  "async-lock",
  "async-task",
- "fastrand",
  "futures-io",
  "futures-lite",
  "piper",
- "tracing",
 ]
 
 [[package]]
@@ -390,9 +389,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.0.95"
+version = "1.0.97"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
+checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
 dependencies = [
  "jobserver",
  "libc",
@@ -481,7 +480,7 @@ dependencies = [
  "heck",
  "proc-macro2",
  "quote",
- "syn 2.0.60",
+ "syn 2.0.63",
 ]
 
 [[package]]
@@ -510,15 +509,15 @@ dependencies = [
 
 [[package]]
 name = "colorchoice"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
 
 [[package]]
 name = "concurrent-queue"
-version = "2.4.0"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
 dependencies = [
  "crossbeam-utils",
 ]
@@ -538,9 +537,9 @@ dependencies = [
 
 [[package]]
 name = "const_fn"
-version = "0.4.9"
+version = "0.4.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935"
+checksum = "373e9fafaa20882876db20562275ff58d50e0caa2590077fe7ce7bef90211d0d"
 
 [[package]]
 name = "constant_time_eq"
@@ -637,9 +636,9 @@ dependencies = [
 
 [[package]]
 name = "data-encoding"
-version = "2.5.0"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
+checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
 
 [[package]]
 name = "deflate64"
@@ -664,7 +663,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.60",
+ "syn 2.0.63",
 ]
 
 [[package]]
@@ -742,6 +741,17 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
 
+[[package]]
+name = "displaydoc"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.63",
+]
+
 [[package]]
 name = "dunce"
 version = "1.0.4"
@@ -809,9 +819,9 @@ dependencies = [
 
 [[package]]
 name = "errno"
-version = "0.3.8"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
 dependencies = [
  "libc",
  "windows-sys 0.52.0",
@@ -857,9 +867,9 @@ dependencies = [
 
 [[package]]
 name = "event-listener-strategy"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3"
+checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1"
 dependencies = [
  "event-listener 5.3.0",
  "pin-project-lite",
@@ -867,9 +877,9 @@ dependencies = [
 
 [[package]]
 name = "fastrand"
-version = "2.0.2"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
 
 [[package]]
 name = "fd-lock"
@@ -884,9 +894,9 @@ dependencies = [
 
 [[package]]
 name = "flate2"
-version = "1.0.28"
+version = "1.0.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
+checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
 dependencies = [
  "crc32fast",
  "libz-ng-sys",
@@ -950,7 +960,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.60",
+ "syn 2.0.63",
 ]
 
 [[package]]
@@ -1003,9 +1013,9 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.14"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
 dependencies = [
  "cfg-if",
  "libc",
@@ -1070,9 +1080,9 @@ dependencies = [
 
 [[package]]
 name = "hashbrown"
-version = "0.14.3"
+version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
 
 [[package]]
 name = "heck"
@@ -1232,7 +1242,7 @@ dependencies = [
  "futures-util",
  "http 0.2.12",
  "hyper 0.14.28",
- "rustls 0.21.11",
+ "rustls 0.21.12",
  "tokio",
  "tokio-rustls 0.24.1",
 ]
@@ -1350,6 +1360,12 @@ version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
 
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
 [[package]]
 name = "itoa"
 version = "1.0.11"
@@ -1358,9 +1374,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
 
 [[package]]
 name = "jobserver"
-version = "0.1.30"
+version = "0.1.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2"
+checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
 dependencies = [
  "libc",
 ]
@@ -1382,9 +1398,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.153"
+version = "0.2.154"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
 
 [[package]]
 name = "libloading"
@@ -1430,9 +1446,9 @@ checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
 
 [[package]]
 name = "lock_api"
-version = "0.4.11"
+version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
 dependencies = [
  "autocfg",
  "scopeguard",
@@ -1823,9 +1839,9 @@ dependencies = [
 
 [[package]]
 name = "num-traits"
-version = "0.2.18"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 dependencies = [
  "autocfg",
 ]
@@ -1893,9 +1909,9 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae"
 
 [[package]]
 name = "parking_lot"
-version = "0.12.1"
+version = "0.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb"
 dependencies = [
  "lock_api",
  "parking_lot_core",
@@ -1903,22 +1919,22 @@ dependencies = [
 
 [[package]]
 name = "parking_lot_core"
-version = "0.9.9"
+version = "0.9.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
 dependencies = [
  "cfg-if",
  "libc",
- "redox_syscall 0.4.1",
+ "redox_syscall 0.5.1",
  "smallvec",
- "windows-targets 0.48.5",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
 name = "paste"
-version = "1.0.14"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
 
 [[package]]
 name = "path-clean"
@@ -1965,7 +1981,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.60",
+ "syn 2.0.63",
 ]
 
 [[package]]
@@ -2031,9 +2047,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.81"
+version = "1.0.82"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
+checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
 dependencies = [
  "unicode-ident",
 ]
@@ -2054,7 +2070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd"
 dependencies = [
  "quote",
- "syn 2.0.60",
+ "syn 2.0.63",
 ]
 
 [[package]]
@@ -2112,7 +2128,7 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
- "getrandom 0.2.14",
+ "getrandom 0.2.15",
 ]
 
 [[package]]
@@ -2215,11 +2231,11 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
 
 [[package]]
 name = "redox_syscall"
-version = "0.4.1"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
 dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.5.0",
 ]
 
 [[package]]
@@ -2239,7 +2255,7 @@ version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
 dependencies = [
- "getrandom 0.2.14",
+ "getrandom 0.2.15",
  "libredox",
  "thiserror",
 ]
@@ -2311,7 +2327,7 @@ dependencies = [
  "once_cell",
  "percent-encoding",
  "pin-project-lite",
- "rustls 0.21.11",
+ "rustls 0.21.12",
  "rustls-pemfile",
  "serde",
  "serde_json",
@@ -2337,7 +2353,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
 dependencies = [
  "cc",
  "cfg-if",
- "getrandom 0.2.14",
+ "getrandom 0.2.15",
  "libc",
  "spin",
  "untrusted",
@@ -2357,9 +2373,9 @@ dependencies = [
 
 [[package]]
 name = "rmp-serde"
-version = "1.2.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "938a142ab806f18b88a97b0dea523d39e0fd730a064b035726adcfc58a8a5188"
+checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
 dependencies = [
  "byteorder 1.5.0",
  "rmp",
@@ -2380,9 +2396,9 @@ dependencies = [
 
 [[package]]
 name = "rustc-demangle"
-version = "0.1.23"
+version = "0.1.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
 
 [[package]]
 name = "rustc-hash"
@@ -2405,14 +2421,14 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
 dependencies = [
- "semver 1.0.22",
+ "semver 1.0.23",
 ]
 
 [[package]]
 name = "rustix"
-version = "0.38.32"
+version = "0.38.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
 dependencies = [
  "bitflags 2.5.0",
  "errno",
@@ -2423,9 +2439,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.21.11"
+version = "0.21.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4"
+checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
 dependencies = [
  "log",
  "ring",
@@ -2442,7 +2458,7 @@ dependencies = [
  "log",
  "ring",
  "rustls-pki-types",
- "rustls-webpki 0.102.2",
+ "rustls-webpki 0.102.3",
  "subtle",
  "zeroize",
 ]
@@ -2458,9 +2474,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-pki-types"
-version = "1.4.1"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
+checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d"
 
 [[package]]
 name = "rustls-webpki"
@@ -2474,9 +2490,9 @@ dependencies = [
 
 [[package]]
 name = "rustls-webpki"
-version = "0.102.2"
+version = "0.102.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
+checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf"
 dependencies = [
  "ring",
  "rustls-pki-types",
@@ -2507,9 +2523,9 @@ dependencies = [
 
 [[package]]
 name = "ryu"
-version = "1.0.17"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
 
 [[package]]
 name = "same-file"
@@ -2538,9 +2554,9 @@ dependencies = [
 
 [[package]]
 name = "self_cell"
-version = "1.0.3"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "58bf37232d3bb9a2c4e641ca2a11d83b5062066f88df7fed36c28772046d65ba"
+checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a"
 
 [[package]]
 name = "semver"
@@ -2553,9 +2569,9 @@ dependencies = [
 
 [[package]]
 name = "semver"
-version = "1.0.22"
+version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
+checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
 
 [[package]]
 name = "semver-parser"
@@ -2565,9 +2581,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 
 [[package]]
 name = "serde"
-version = "1.0.198"
+version = "1.0.201"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
+checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c"
 dependencies = [
  "serde_derive",
 ]
@@ -2584,20 +2600,20 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.198"
+version = "1.0.201"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
+checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.60",
+ "syn 2.0.63",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.116"
+version = "1.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
+checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
 dependencies = [
  "indexmap",
  "itoa",
@@ -2682,9 +2698,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
 
 [[package]]
 name = "signal-hook-registry"
-version = "1.4.1"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
 dependencies = [
  "libc",
 ]
@@ -2712,9 +2728,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
 
 [[package]]
 name = "socket2"
-version = "0.5.6"
+version = "0.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
+checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
 dependencies = [
  "libc",
  "windows-sys 0.52.0",
@@ -2809,9 +2825,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.60"
+version = "2.0.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
+checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2859,22 +2875,22 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.59"
+version = "1.0.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
+checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.59"
+version = "1.0.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
+checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.60",
+ "syn 2.0.63",
 ]
 
 [[package]]
@@ -2998,7 +3014,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.60",
+ "syn 2.0.63",
 ]
 
 [[package]]
@@ -3007,7 +3023,7 @@ version = "0.24.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
 dependencies = [
- "rustls 0.21.11",
+ "rustls 0.21.12",
  "tokio",
 ]
 
@@ -3040,16 +3056,15 @@ dependencies = [
 
 [[package]]
 name = "tokio-util"
-version = "0.7.10"
+version = "0.7.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
+checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
 dependencies = [
  "bytes",
  "futures-core",
  "futures-sink",
  "pin-project-lite",
  "tokio",
- "tracing",
 ]
 
 [[package]]
@@ -3135,7 +3150,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.60",
+ "syn 2.0.63",
 ]
 
 [[package]]
@@ -3245,9 +3260,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
 
 [[package]]
 name = "unicode-width"
-version = "0.1.11"
+version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
+checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
 
 [[package]]
 name = "unsafe-libyaml"
@@ -3354,7 +3369,7 @@ dependencies = [
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.60",
+ "syn 2.0.63",
  "wasm-bindgen-shared",
 ]
 
@@ -3388,7 +3403,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.60",
+ "syn 2.0.63",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
@@ -3442,11 +3457,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
 [[package]]
 name = "winapi-util"
-version = "0.1.6"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
 dependencies = [
- "winapi",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -3605,9 +3620,9 @@ checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
 
 [[package]]
 name = "winnow"
-version = "0.6.6"
+version = "0.6.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
+checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d"
 dependencies = [
  "memchr",
 ]
@@ -3642,27 +3657,45 @@ name = "zeroize"
 version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.63",
+]
 
 [[package]]
 name = "zip"
-version = "1.1.0"
+version = "1.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f21968e6da56f847a155a89581ba846507afa14854e041f3053edb6ddd19f807"
+checksum = "c700ea425e148de30c29c580c1f9508b93ca57ad31c9f4e96b83c194c37a7a8f"
 dependencies = [
  "aes",
  "arbitrary",
- "byteorder 1.5.0",
  "bzip2",
  "constant_time_eq 0.3.0",
  "crc32fast",
  "crossbeam-utils",
  "deflate64",
+ "displaydoc",
  "flate2",
  "hmac",
+ "indexmap",
  "lzma-rs",
  "pbkdf2",
+ "rand",
  "sha1 0.10.6",
+ "thiserror",
  "time 0.3.36",
+ "zeroize",
  "zopfli",
  "zstd",
 ]

From 816b6654da15d3caa4f345d4f2c5b3a7f8e9f936 Mon Sep 17 00:00:00 2001
From: Filip Tibell <filip.tibell@gmail.com>
Date: Sun, 12 May 2024 14:07:30 +0200
Subject: [PATCH 3/7] Update changelog

---
 CHANGELOG.md           | 15 +++++++++++++++
 crates/lune/src/lib.rs |  1 +
 2 files changed, 16 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b1d435..78086e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,12 +51,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   Currently supported targets are the same as the ones included with each
   release of Lune on GitHub. Check releases for a full list of targets.
 
+- Split the repository into modular crates instead of a monolith. ([#188])
+
+  If you previously depended on Lune as a crate, nothing about it has changed for version `0.8.4`, but now each individual sub-crate has also been published and is available for use:
+
+  - `lune` (old)
+  - `lune-utils`
+  - `lune-roblox`
+  - `lune-std-*` for every builtin library
+
+  When depending on the main `lune` crate, each builtin library also has a feature flag that can be toggled in the format `std-*`.
+
+  In general, this should mean that it is now much easier to make your own Lune builtin, publish your own flavor of a Lune CLI, or take advantage of all the work that has been done for Lune as a runtime when making your own Rust programs.
+
 - Added `stdio.readToEnd()` for reading the entire stdin passed to Lune
 - Changed the `User-Agent` header in `net.request` to be more descriptive ([#186])
 - Updated to Luau version `0.622`.
 
 ### Fixed
 
+- Fixed not being able to decompress `lz4` format in high compression mode
 - Fixed stack overflow for tables with circular keys ([#183])
 - Fixed `net.serve` no longer accepting ipv6 addresses
 - Fixed headers in `net.serve` being raw bytes instead of strings
@@ -65,6 +79,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 [#162]: https://github.com/lune-org/lune/pull/162
 [#183]: https://github.com/lune-org/lune/pull/183
 [#186]: https://github.com/lune-org/lune/pull/186
+[#188]: https://github.com/lune-org/lune/pull/188
 
 ## `0.8.3` - April 15th, 2024
 
diff --git a/crates/lune/src/lib.rs b/crates/lune/src/lib.rs
index 1a53abb..ef3cd86 100644
--- a/crates/lune/src/lib.rs
+++ b/crates/lune/src/lib.rs
@@ -2,6 +2,7 @@
 
 mod rt;
 
+// TODO: Remove this in 0.9.0 since it is now available as a separate crate!
 #[cfg(feature = "std-roblox")]
 pub use lune_roblox as roblox;
 

From 763b3ff6a74f75097c767b57f831da8552da5f89 Mon Sep 17 00:00:00 2001
From: Filip Tibell <filip.tibell@gmail.com>
Date: Sun, 12 May 2024 14:08:12 +0200
Subject: [PATCH 4/7] Version 0.8.4

---
 CHANGELOG.md           | 2 +-
 Cargo.lock             | 2 +-
 crates/lune/Cargo.toml | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 78086e6..be64496 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,7 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## Unreleased
+## `0.8.4` - May 12th, 2024
 
 ### Changed
 
diff --git a/Cargo.lock b/Cargo.lock
index a4cfe3a..988c44a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1471,7 +1471,7 @@ dependencies = [
 
 [[package]]
 name = "lune"
-version = "0.8.3"
+version = "0.8.4"
 dependencies = [
  "anyhow",
  "clap",
diff --git a/crates/lune/Cargo.toml b/crates/lune/Cargo.toml
index af528d9..4e43302 100644
--- a/crates/lune/Cargo.toml
+++ b/crates/lune/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lune"
-version = "0.8.3"
+version = "0.8.4"
 edition = "2021"
 license = "MPL-2.0"
 repository = "https://github.com/lune-org/lune"

From 6bc1fa2343a9d6e791e99571dadc3222a01e9f48 Mon Sep 17 00:00:00 2001
From: Filip Tibell <filip.tibell@gmail.com>
Date: Sun, 12 May 2024 14:10:50 +0200
Subject: [PATCH 5/7] Fix release workflow version read

---
 .github/workflows/release.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 12f0e91..d794e2b 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -24,7 +24,7 @@ jobs:
         uses: SebRollen/toml-action@v1.2.0
         id: get_version
         with:
-          file: Cargo.toml
+          file: crates/lune/Cargo.toml
           field: package.version
 
   dry-run:

From e0b9ceb86df9eb978bccd7baa151724b3f778aff Mon Sep 17 00:00:00 2001
From: Filip Tibell <filip.tibell@gmail.com>
Date: Sun, 12 May 2024 14:32:39 +0200
Subject: [PATCH 6/7] Add missing repository and description fields to all
 manifests

---
 crates/lune-roblox/Cargo.toml       | 2 ++
 crates/lune-std-datetime/Cargo.toml | 2 ++
 crates/lune-std-fs/Cargo.toml       | 2 ++
 crates/lune-std-luau/Cargo.toml     | 2 ++
 crates/lune-std-net/Cargo.toml      | 2 ++
 crates/lune-std-process/Cargo.toml  | 2 ++
 crates/lune-std-regex/Cargo.toml    | 2 ++
 crates/lune-std-roblox/Cargo.toml   | 2 ++
 crates/lune-std-serde/Cargo.toml    | 2 ++
 crates/lune-std-stdio/Cargo.toml    | 2 ++
 crates/lune-std-task/Cargo.toml     | 2 ++
 crates/lune-std/Cargo.toml          | 2 ++
 crates/lune-utils/Cargo.toml        | 2 ++
 13 files changed, 26 insertions(+)

diff --git a/crates/lune-roblox/Cargo.toml b/crates/lune-roblox/Cargo.toml
index 88eb441..b20bd50 100644
--- a/crates/lune-roblox/Cargo.toml
+++ b/crates/lune-roblox/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-roblox"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Roblox library for Lune"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-std-datetime/Cargo.toml b/crates/lune-std-datetime/Cargo.toml
index 34bef78..6cb5499 100644
--- a/crates/lune-std-datetime/Cargo.toml
+++ b/crates/lune-std-datetime/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-std-datetime"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Lune standard library - DateTime"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-std-fs/Cargo.toml b/crates/lune-std-fs/Cargo.toml
index a3bc219..033e60d 100644
--- a/crates/lune-std-fs/Cargo.toml
+++ b/crates/lune-std-fs/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-std-fs"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Lune standard library - FS"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-std-luau/Cargo.toml b/crates/lune-std-luau/Cargo.toml
index d01584c..a4d1b23 100644
--- a/crates/lune-std-luau/Cargo.toml
+++ b/crates/lune-std-luau/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-std-luau"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Lune standard library - Luau"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-std-net/Cargo.toml b/crates/lune-std-net/Cargo.toml
index 966e374..2cf086e 100644
--- a/crates/lune-std-net/Cargo.toml
+++ b/crates/lune-std-net/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-std-net"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Lune standard library - Net"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-std-process/Cargo.toml b/crates/lune-std-process/Cargo.toml
index 211d875..42d720b 100644
--- a/crates/lune-std-process/Cargo.toml
+++ b/crates/lune-std-process/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-std-process"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Lune standard library - Process"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-std-regex/Cargo.toml b/crates/lune-std-regex/Cargo.toml
index a7dc859..5bebcb5 100644
--- a/crates/lune-std-regex/Cargo.toml
+++ b/crates/lune-std-regex/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-std-regex"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Lune standard library - RegEx"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-std-roblox/Cargo.toml b/crates/lune-std-roblox/Cargo.toml
index af051c7..269aad9 100644
--- a/crates/lune-std-roblox/Cargo.toml
+++ b/crates/lune-std-roblox/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-std-roblox"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Lune standard library - Roblox"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-std-serde/Cargo.toml b/crates/lune-std-serde/Cargo.toml
index 4f8faef..fdc92f4 100644
--- a/crates/lune-std-serde/Cargo.toml
+++ b/crates/lune-std-serde/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-std-serde"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Lune standard library - Serde"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-std-stdio/Cargo.toml b/crates/lune-std-stdio/Cargo.toml
index 464f5d9..7d3909e 100644
--- a/crates/lune-std-stdio/Cargo.toml
+++ b/crates/lune-std-stdio/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-std-stdio"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Lune standard library - Stdio"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-std-task/Cargo.toml b/crates/lune-std-task/Cargo.toml
index 4df2d8a..edc6e5a 100644
--- a/crates/lune-std-task/Cargo.toml
+++ b/crates/lune-std-task/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-std-task"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Lune standard library - Task"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-std/Cargo.toml b/crates/lune-std/Cargo.toml
index 3ee4c9a..ed8416d 100644
--- a/crates/lune-std/Cargo.toml
+++ b/crates/lune-std/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-std"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Lune standard library"
 
 [lib]
 path = "src/lib.rs"
diff --git a/crates/lune-utils/Cargo.toml b/crates/lune-utils/Cargo.toml
index dd39731..f07b737 100644
--- a/crates/lune-utils/Cargo.toml
+++ b/crates/lune-utils/Cargo.toml
@@ -3,6 +3,8 @@ name = "lune-utils"
 version = "0.1.0"
 edition = "2021"
 license = "MPL-2.0"
+repository = "https://github.com/lune-org/lune"
+description = "Utilities library for Lune"
 
 [lib]
 path = "src/lib.rs"

From 962a2e50be3638b0dd64fb90ffe25208e4ca13ca Mon Sep 17 00:00:00 2001
From: Erica Marigold <hi@devcomp.xyz>
Date: Sun, 12 May 2024 19:59:24 +0530
Subject: [PATCH 7/7] refactor: migrate to new project structure (see
 https://github.com/lune-org/lune/commit/de71558c5df94e7cb466acb9e2810ebcfb3028a4)

---
 Cargo.lock                                 | 30 +++++++++++++++++-----
 crates/lune-roblox/src/instance/terrain.rs |  8 +++---
 crates/lune/Cargo.toml                     |  2 +-
 crates/lune/src/cli/run.rs                 |  6 ++---
 crates/lune/src/rt/runtime.rs              | 27 ++++++++++---------
 crates/lune/src/standalone/mod.rs          |  8 +++---
 crates/lune/src/tests.rs                   |  4 +--
 7 files changed, 52 insertions(+), 33 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index 988c44a..fdfa079 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1485,7 +1485,7 @@ dependencies = [
  "lune-std",
  "lune-utils",
  "mlua",
- "mlua-luau-scheduler",
+ "mlua-luau-scheduler 0.0.2 (git+https://github.com/0x5eal/mlua-luau-scheduler-exitstatus.git)",
  "once_cell",
  "reqwest",
  "rustyline",
@@ -1531,7 +1531,7 @@ dependencies = [
  "lune-std-task",
  "lune-utils",
  "mlua",
- "mlua-luau-scheduler",
+ "mlua-luau-scheduler 0.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde",
  "serde_json",
  "tokio",
@@ -1581,7 +1581,7 @@ dependencies = [
  "lune-std-serde",
  "lune-utils",
  "mlua",
- "mlua-luau-scheduler",
+ "mlua-luau-scheduler 0.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "reqwest",
  "tokio",
  "tokio-tungstenite",
@@ -1595,7 +1595,7 @@ dependencies = [
  "directories",
  "lune-utils",
  "mlua",
- "mlua-luau-scheduler",
+ "mlua-luau-scheduler 0.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "os_str_bytes",
  "pin-project",
  "tokio",
@@ -1618,7 +1618,7 @@ dependencies = [
  "lune-roblox",
  "lune-utils",
  "mlua",
- "mlua-luau-scheduler",
+ "mlua-luau-scheduler 0.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "once_cell",
  "rbx_cookie",
 ]
@@ -1646,7 +1646,7 @@ dependencies = [
  "dialoguer",
  "lune-utils",
  "mlua",
- "mlua-luau-scheduler",
+ "mlua-luau-scheduler 0.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio",
 ]
 
@@ -1656,7 +1656,7 @@ version = "0.1.0"
 dependencies = [
  "lune-utils",
  "mlua",
- "mlua-luau-scheduler",
+ "mlua-luau-scheduler 0.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "tokio",
 ]
 
@@ -1779,6 +1779,22 @@ dependencies = [
  "tracing",
 ]
 
+[[package]]
+name = "mlua-luau-scheduler"
+version = "0.0.2"
+source = "git+https://github.com/0x5eal/mlua-luau-scheduler-exitstatus.git#82c8b902e09a21a9befa4e037beadefbe2360b7f"
+dependencies = [
+ "async-executor",
+ "blocking",
+ "concurrent-queue",
+ "derive_more",
+ "event-listener 4.0.3",
+ "futures-lite",
+ "mlua",
+ "rustc-hash",
+ "tracing",
+]
+
 [[package]]
 name = "mlua-sys"
 version = "0.5.2"
diff --git a/crates/lune-roblox/src/instance/terrain.rs b/crates/lune-roblox/src/instance/terrain.rs
index 852f321..f39d59e 100644
--- a/crates/lune-roblox/src/instance/terrain.rs
+++ b/crates/lune-roblox/src/instance/terrain.rs
@@ -27,11 +27,13 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M)
 }
 
 fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
-    if let Some(Variant::MaterialColors(material_colors)) = instance.get_property("MaterialColors")
+    if let Variant::MaterialColors(inner) = instance
+        .get_property("MaterialColors")
+        .unwrap_or(Variant::MaterialColors(MaterialColors::default()))
     {
-        material_colors
+        inner
     } else {
-        MaterialColors::default()
+        unreachable!()
     }
 }
 
diff --git a/crates/lune/Cargo.toml b/crates/lune/Cargo.toml
index 4e43302..1afb52a 100644
--- a/crates/lune/Cargo.toml
+++ b/crates/lune/Cargo.toml
@@ -57,7 +57,7 @@ workspace = true
 
 [dependencies]
 mlua = { version = "0.9.7", features = ["luau"] }
-mlua-luau-scheduler = "0.0.2"
+mlua-luau-scheduler = { git = "https://github.com/0x5eal/mlua-luau-scheduler-exitstatus.git" }
 
 anyhow = "1.0"
 console = "0.15"
diff --git a/crates/lune/src/cli/run.rs b/crates/lune/src/cli/run.rs
index 35e523e..fcce205 100644
--- a/crates/lune/src/cli/run.rs
+++ b/crates/lune/src/cli/run.rs
@@ -41,8 +41,8 @@ impl RunCommand {
         };
 
         // Create a new lune object with all globals & run the script
-        let result = Runtime::new()
-            .with_args(self.script_args)
+        let mut runtime = Runtime::new().with_args(self.script_args);
+        let result = runtime
             .run(&script_display_name, strip_shebang(script_contents))
             .await;
         Ok(match result {
@@ -50,7 +50,7 @@ impl RunCommand {
                 eprintln!("{err}");
                 ExitCode::FAILURE
             }
-            Ok(code) => code,
+            Ok((code, _)) => ExitCode::from(code),
         })
     }
 }
diff --git a/crates/lune/src/rt/runtime.rs b/crates/lune/src/rt/runtime.rs
index a5501c6..0499b3b 100644
--- a/crates/lune/src/rt/runtime.rs
+++ b/crates/lune/src/rt/runtime.rs
@@ -1,7 +1,6 @@
 #![allow(clippy::missing_panics_doc)]
 
 use std::{
-    process::ExitCode,
     rc::Rc,
     sync::{
         atomic::{AtomicBool, Ordering},
@@ -9,7 +8,7 @@ use std::{
     },
 };
 
-use mlua::Lua;
+use mlua::{IntoLuaMulti as _, Lua, Value};
 use mlua_luau_scheduler::Scheduler;
 
 use super::{RuntimeError, RuntimeResult};
@@ -82,7 +81,7 @@ impl Runtime {
         &mut self,
         script_name: impl AsRef<str>,
         script_contents: impl AsRef<[u8]>,
-    ) -> RuntimeResult<ExitCode> {
+    ) -> RuntimeResult<(u8, Vec<Value>)> {
         // Create a new scheduler for this run
         let sched = Scheduler::new(&self.lua);
 
@@ -101,16 +100,20 @@ impl Runtime {
             .set_name(script_name.as_ref());
 
         // Run it on our scheduler until it and any other spawned threads complete
-        sched.push_thread_back(main, ())?;
+        let main_thread_id = sched.push_thread_back(main, ())?;
         sched.run().await;
 
-        // Return the exit code - default to FAILURE if we got any errors
-        Ok(sched.get_exit_code().unwrap_or({
-            if got_any_error.load(Ordering::SeqCst) {
-                ExitCode::FAILURE
-            } else {
-                ExitCode::SUCCESS
-            }
-        }))
+        let thread_res = match sched.get_thread_result(main_thread_id) {
+            Some(res) => res,
+            None => Value::Nil.into_lua_multi(&self.lua),
+        }?
+        .into_vec();
+
+        Ok((
+            sched
+                .get_exit_code()
+                .unwrap_or(u8::from(got_any_error.load(Ordering::SeqCst))),
+            thread_res,
+        ))
     }
 }
diff --git a/crates/lune/src/standalone/mod.rs b/crates/lune/src/standalone/mod.rs
index fe58913..805eb95 100644
--- a/crates/lune/src/standalone/mod.rs
+++ b/crates/lune/src/standalone/mod.rs
@@ -29,16 +29,14 @@ pub async fn run(patched_bin: impl AsRef<[u8]>) -> Result<ExitCode> {
     let args = env::args().skip(1).collect::<Vec<_>>();
     let meta = Metadata::from_bytes(patched_bin).expect("must be a standalone binary");
 
-    let result = Runtime::new()
-        .with_args(args)
-        .run("STANDALONE", meta.bytecode)
-        .await;
+    let mut rt = Runtime::new().with_args(args);
+    let result = rt.run("STANDALONE", meta.bytecode).await;
 
     Ok(match result {
         Err(err) => {
             eprintln!("{err}");
             ExitCode::FAILURE
         }
-        Ok(code) => code,
+        Ok((code, _)) => ExitCode::from(code),
     })
 }
diff --git a/crates/lune/src/tests.rs b/crates/lune/src/tests.rs
index ae599da..7a5f5a7 100644
--- a/crates/lune/src/tests.rs
+++ b/crates/lune/src/tests.rs
@@ -42,8 +42,8 @@ macro_rules! create_tests {
 				.trim_end_matches(".luau")
 				.trim_end_matches(".lua")
 				.to_string();
-            let exit_code = lune.run(&script_name, &script).await?;
-            Ok(exit_code)
+            let (exit_code, _) = lune.run(&script_name, &script).await?;
+            Ok(ExitCode::from(exit_code))
         }
     )* }
 }