local process = require("@lune/process")

local logger = require("./logger").new(4)

return function(command, args, options, ignoreErrors)
	local data = process.spawn(command, args, options or {
		stdio = "default",
	})

	if data.stderr ~= "" and not ignoreErrors then
		logger:error(`Error occurred when running \"{command}\" (code {data.code}): \n{data.stderr}`)
	end

	return data
end


local CrateConfig = require("@mgr/specifiers/CrateConfig")

return CrateConfig {
    name = "core",
    description = "A featherweight framework for Luau",
    version = "0.1.0-alpha.2",
    lib = "lib",

    targets = {
        luau = true,
        roblox = true,
    },
}


local lib = require("@self/lib")
export type Singleton = lib.Singleton

export type Lifecycle = lib.Lifecycle

export type LifecycleHook = lib.LifecycleHook

export type Phase = lib.Phase

export type PhaseHook = lib.PhaseHook

export type Identity = lib.Identity

export type Mod = lib.Mod
return lib


export type Singleton = {
    init: ((Singleton) -> ())?,
    start: ((Singleton) -> ())?,
    [any]: any,
}

export type Lifecycle = | "loaded" | "injected" | "initialized" | "started"
export type LifecycleHook = (lifecycle: Lifecycle, singleton: Singleton) -> ()

export type Phase =
    | "pre_loading"
    | "post_loading"
    | "pre_initialization"
    | "post_initialization"
    | "pre_starting"
    | "post_starting"
export type PhaseHook = (phase: Phase, singletons: { Singleton }) -> ()

local Quill = {}
local mt = { __index = Quill }
export type Identity = setmetatable<{
    modules: { ModuleScript },
    singletons: { Singleton },

    lifecycle_hooks: { LifecycleHook },
    phase_hooks: { PhaseHook },
}, typeof(mt)>

local function constructor(): Identity
    local self = {
        modules = {},
        singletons = {},

        lifecycle_hooks = {},
        phase_hooks = {},
    }
    return setmetatable(self, mt)
end

function Quill.with_module(self: Identity, module: ModuleScript): Identity
    table.insert(self.modules, module)
    return self
end

function Quill.with_modules(self: Identity, modules: { ModuleScript }): Identity
    table.move(modules, 1, #modules, #self.modules + 1, self.modules)
    return self
end

local function no_yield<A..., R...>(fn: (A...) -> R..., ...: A...)
    local thread = coroutine.create(fn)

    local ok, message = coroutine.resume(thread, ...)
    if not ok then
        error(debug.traceback(thread, message), 3)
    end

    if coroutine.status(thread) ~= "dead" then
        error(
            debug.traceback(thread, "Attempted to yield in a noyield call"),
            2
        )
    end
end

local function call_closures<T..., R...>(hooks: { (T...) -> R... }, ...: T...)
    for _, hook in hooks do
        no_yield(hook, ...)
    end
end

--- Util function for better types
local function call_lifecycle_hooks(
    hooks: { LifecycleHook },
    lifecycle: Lifecycle,
    singleton: Singleton
)
    call_closures(hooks, lifecycle, singleton)
end

--- Util function for better types
local function call_phase_hooks(
    hooks: { PhaseHook },
    phase: Phase,
    singletons: { Singleton }
)
    call_closures(hooks, phase, singletons)
end

function Quill.with_singleton(self: Identity, singleton: Singleton): Identity
    table.insert(self.singletons, singleton)
    call_lifecycle_hooks(self.lifecycle_hooks, "injected", singleton)
    return self
end

function Quill.with_singletons(
    self: Identity,
    singletons: { Singleton }
): Identity
    for _, singleton in singletons do
        table.insert(self.singletons, singleton)
        call_lifecycle_hooks(self.lifecycle_hooks, "injected", singleton)
    end
    return self
end

export type Mod = {
    on_lifecycle: LifecycleHook?,
    on_phase: PhaseHook?,
}

--- Hooks can get expensive as they're ran on every single phase / lifecycle. Use them sparingly.
--- Hooks cannot yield.
function Quill.with_mod(self: Identity, mod: Mod): Identity
    if mod.on_lifecycle then
        table.insert(self.lifecycle_hooks, mod.on_lifecycle)
    end

    if mod.on_phase then
        table.insert(self.phase_hooks, mod.on_phase)
    end

    return self
end

function Quill.write(self: Identity)
    local clock_begin = os.clock()

    call_phase_hooks(self.phase_hooks, "pre_loading", {})
    for _, module in self.modules do
        local singleton = (require)(module)
        table.insert(self.singletons :: any, singleton)
        call_lifecycle_hooks(self.lifecycle_hooks, "loaded", singleton)
    end

    call_phase_hooks(self.phase_hooks, "post_loading", self.singletons)

    call_phase_hooks(self.phase_hooks, "pre_initialization", self.singletons)
    for _, singleton in self.singletons do
        if singleton.init then
            no_yield(singleton.init, singleton)
        end

        call_lifecycle_hooks(self.lifecycle_hooks, "initialized", singleton)
    end
    call_phase_hooks(self.phase_hooks, "post_initialization", self.singletons)

    call_phase_hooks(self.phase_hooks, "pre_starting", self.singletons)
    for _, singleton in self.singletons do
        if singleton.start then
            task.spawn(singleton.start, singleton)
        end

        call_lifecycle_hooks(self.lifecycle_hooks, "started", singleton)
    end
    call_phase_hooks(self.phase_hooks, "post_starting", self.singletons)

    local clock_end = os.clock()
    local time_took = math.round((clock_end - clock_begin) * 1000)

    print(`[🪶] Started {#self.singletons} singletons in {time_took}ms`)
end

local quill = table.freeze { new = constructor }

return quill


local CrateConfig = require("@mgr/specifiers/CrateConfig")

return CrateConfig {
    name = "dependencies",
    description = "A dependency management solution for Quill",
    version = "0.1.0-alpha.3",
    lib = "lib",

    targets = {
        luau = true,
        roblox = true,
    },

    workspace_dependencies = {
        core = true,
    },
}


local lib = require("@self/lib")

export type SortingMethod = lib.SortingMethod
return lib


local core = require("@crates/core")

local function use<T>(singleton: T): T
    return { UNINITIALIZED_DEPENDENCY = true, singleton = singleton } :: any
end

type AdjacencyList<T> = { [T]: { T } }
--- Kahn's algorithm for topological sorting of adjacency lists
local function toposort<T>(graph: AdjacencyList<T>): { T }
    local in_degree: { [T]: number } = {}
    for node, neighbors in graph do
        if not in_degree[node] then
            in_degree[node] = 0
        end

        for _, neighbor in neighbors do
            if not in_degree[neighbor] then
                in_degree[neighbor] = 0
            end
            (in_degree :: any)[neighbor] += 1
        end
    end

    local queue: { T } = {}
    for node, degree in in_degree do
        if degree == 0 then
            table.insert(queue, node)
        end
    end

    local sorted_order: { T } = {}
    while #queue > 0 do
        local node = table.remove(queue, 1)
        table.insert(sorted_order, node)
        for _, neighbor: T in graph[node] or {} do
            local degree = in_degree[neighbor] - 1
            in_degree[neighbor] = degree
            if degree == 0 then
                table.insert(queue, neighbor)
            end
        end
    end

    return sorted_order
end

local function priority_sort(singletons: { core.Singleton })
    table.sort(singletons, function(a: Singleton, b: Singleton)
        if not a.priority then
            return b.priority == nil
        end
        if not b.priority then
            return true
        end
        return a.priority > b.priority
    end)
end

type Singleton = core.Singleton
export type SortingMethod = | "topological" | "priority"
local function mod(sorting_method: SortingMethod?): core.Mod
    sorting_method = sorting_method or "topological"
    local dependencies_list: AdjacencyList<Singleton> = {}
    local to_inject: { [Singleton]: { [string]: Singleton } } = {}
    local function lifecycle_hook(
        lifecycle: core.Lifecycle,
        singleton: core.Singleton
    )
        if lifecycle ~= "loaded" then
            return
        end

        local dependencies = {}
        local dependencies_map = {}
        for name, value in singleton do
            if
                type(value) ~= "table"
                or value.UNINITIALIZED_DEPENDENCY ~= true
            then
                continue
            end

            table.insert(dependencies :: any, value.singleton)
            dependencies_map[name] = value.singleton
        end
        dependencies_list[singleton] = dependencies
        to_inject[singleton] = dependencies_map
    end

    local function phase_hook(phase: core.Phase, singletons: { Singleton })
        if phase == "pre_loading" then
            if sorting_method == "priority" then
                priority_sort(singletons)
            elseif sorting_method == "topological" then
                local sorted = toposort(dependencies_list)
                table.clear(singletons)
                for idx = #sorted, 1, -1 do
                    table.insert(singletons, sorted[idx])
                end
            end
        elseif phase == "post_loading" then
            if sorting_method ~= "topological" then
                return
            end

            for _, singleton in singletons do
                for name, dependency in to_inject[singleton] do
                    singleton[name] = dependency
                end
            end
        end
    end

    return {
        on_lifecycle = lifecycle_hook,
        on_phase = phase_hook,
    }
end

local rblx_dependencies = table.freeze {
    use = use,
    mod = mod,
}

return rblx_dependencies


local CrateConfig = require("@mgr/specifiers/CrateConfig")

return CrateConfig {
    name = "logger",
    description = "A tiny, context aware logger for Quill",
    version = "0.1.0-alpha.2",
    lib = "lib",

    targets = {
        luau = true,
        roblox = true,
    },
}


local lib = require("@self/lib")
export type Level = lib.Level

export type Event = lib.Event

export type Logger = lib.Logger
return lib


local pretty_print = require("@self/pretty_print")
export type Level = "trace" | "debug" | "info" | "warn" | "error" | "fatal"
export type Event = {
    timestamp: number,
    level: Level,
    message: string,
    logger: string,
    context: { [string]: any }?,
}

export type Logger = {
    --- The logger's identifier.
    _identifier: string,
    --- The logger's context.\
    --- Included in every single event.
    _context: { [string]: any }?,
    --- The logger's subscriber.
    _subscriber: (Event) -> (),

    --- Creates a new logger which inherits this logger's subscriber and context in addition to any new passed context.
    extend: (self: Logger, context: { [string]: any }) -> Logger,
    --- Creates an event with the given level and message for the current system time.\
    --- Accepts additional context which the logger's context will be merged into.
    event: (
        self: Logger,
        level: Level,
        message: string,
        context: { [string]: any }?
    ) -> (),

    --- Creates a trace event with the specified message.\
    --- Accepts additional context which the logger's context will be merged into.
    trace: (self: Logger, message: string, context: { [string]: any }?) -> (),
    --- Creates a debug event with the specified message.\
    --- Accepts additional context which the logger's context will be merged into.
    debug: (self: Logger, message: string, context: { [string]: any }?) -> (),
    --- Creates a info event with the specified message.\
    --- Accepts additional context which the logger's context will be merged into.
    info: (self: Logger, message: string, context: { [string]: any }?) -> (),
    --- Creates a warn event with the specified message.\
    --- Accepts additional context which the logger's context will be merged into.
    warn: (self: Logger, message: string, context: { [string]: any }?) -> (),
    --- Creates an error event with the specified message.\
    --- Accepts additional context which the logger's context will be merged into.
    error: (self: Logger, message: string, context: { [string]: any }?) -> (),
    --- Creates a fatal event with the specified message.\
    --- Accepts additional context which the logger's context will be merged into.
    fatal: (self: Logger, message: string, context: { [string]: any }?) -> (),
}

local LEVELS: { [Level]: number } = {
    trace = 1,
    debug = 2,
    info = 3,
    warn = 4,
    error = 5,
    fatal = 6,
}

local LOG_LEVEL = 1

local Logger = {}
local mt = { __index = Logger }

--- Constructs a new logger.
local function constructor(
    identifier: string,
    subscriber: (Event) -> (),
    context: { [string]: any }?
): Logger
    return setmetatable({
        _identifier = identifier,
        _context = context,
        _subscriber = subscriber,
    }, mt) :: Logger
end

--- Sets the global log level, default being trace.\
--- Any events below this level won't be logged.
local function set_log_level(level: Level)
    LOG_LEVEL = LEVELS[level]
end

function Logger.extend(
    self: Logger,
    identifier: string,
    context: { [string]: any }
): Logger
    local new_context = table.clone(self._context or {})
    for key, value in context do
        new_context[key] = value
    end

    return setmetatable({
        _identifier = identifier,
        _context = new_context,
        _subscriber = self._subscriber,
    }, mt) :: Logger
end

function Logger.event(
    self: Logger,
    level: Level,
    message: string,
    context: { [string]: any }?
)
    if LEVELS[level] < LOG_LEVEL then
        return
    end

    local final_context = self._context
    if final_context and context then
        final_context = table.clone(final_context)
        for key, value in context do
            final_context[key] = value
        end
    end

    local event: Event = {
        timestamp = os.time(),
        level = level,
        message = message,
        logger = self._identifier,
        context = final_context,
    }

    local ok, result = pcall(self._subscriber, event)

    if not ok then
        warn(`Error in subscriber: {result}`)
    end
end

function Logger.trace(
    self: Logger,
    message: string,
    context: { [string]: any }?
)
    self:event("trace", message, context)
end

function Logger.debug(
    self: Logger,
    message: string,
    context: { [string]: any }?
)
    self:event("debug", message, context)
end

function Logger.info(self: Logger, message: string, context: { [string]: any }?)
    self:event("info", message, context)
end

function Logger.warn(self: Logger, message: string, context: { [string]: any }?)
    self:event("warn", message, context)
end

function Logger.error(
    self: Logger,
    message: string,
    context: { [string]: any }?
)
    self:event("error", message, context)
end

function Logger.fatal(
    self: Logger,
    message: string,
    context: { [string]: any }?
)
    self:event("fatal", message, context)
end

local function is_table_empty<T>(tbl: T & {}): boolean
    for _ in tbl :: any do
        return false
    end
    return true
end

local function console_subscriber(event: Event)
    local message =
        `{event.logger} - {event.level} @ {event.timestamp}:\n{event.message}`

    local level = LEVELS[event.level]
    if level < 4 then
        print(message)
    elseif level < 6 then
        warn(message)
    else
        error(message)
    end
end

local function traceback_subscriber(event: Event)
    local traceback = debug.traceback(nil, 5)
    local message =
        `{event.logger} - {event.level} @ {event.timestamp}:\n{event.message}\n{traceback}`

    local level = LEVELS[event.level]
    if level < 4 then
        print(message)
    elseif level < 6 then
        warn(message)
    else
        error(message)
    end
end

local function context_subscriber(event: Event)
    local traceback = debug.traceback(nil, 5)
    local message = if event.context and not is_table_empty(event.context)
        then `{event.logger} - {event.level} @ {event.timestamp}:\n{event.message}\n{pretty_print(
            event.context
        )}\n{traceback}`
        else `{event.logger} - {event.level} @ {event.timestamp}:\n{event.message}\n{traceback}`

    local level = LEVELS[event.level]
    if level < 4 then
        print(message)
    elseif level < 6 then
        warn(message)
    else
        error(message)
    end
end

local logger = table.freeze {
    new = constructor,
    set_log_level = set_log_level,

    console_subscriber = console_subscriber,
    traceback_subscriber = traceback_subscriber,
    context_subscriber = context_subscriber,
}

return logger


local CrateConfig = require("@mgr/specifiers/CrateConfig")

return CrateConfig {
    name = "rblx_components",
    description = "A simple components system for Quill",
    version = "0.1.0-alpha.2",
    lib = "lib",

    targets = {
        roblox = true,
    },

    workspace_dependencies = {
        core = true,
    },
}


local lib = require("@self/lib")

export type ComponentInfo<T> = lib.ComponentInfo<T>

export type CreationData<T> = lib.CreationData<T>
return lib


--# selene: allow(undefined_variable)
local rblx_types = require("./rblx_types")

type luau_vector = vector
type rbx_vector2 = Vector2
type rbx_vector3 = Vector3
type rbx_udim = UDim
type rbx_udim2 = UDim2
type rbx_brickcolor = BrickColor
type rbx_color3 = Color3
type rbx_cframe = CFrame
type rbx_numbersequence = NumberSequence
type rbx_colorsequence = ColorSequence
type rbx_numberrange = NumberRange
type rbx_rect = Rect
type rbx_font = Font
export type function TypeOfKind(kind: type): type
    local singleton = kind:is("singleton") and kind:value()
    assert(singleton ~= nil, "The passed type isn't a singleton")

    if singleton == "string" then
        return types.string
    elseif singleton == "number" then
        return types.number
    elseif singleton == "boolean" then
        return types.boolean
    elseif singleton == "vector" then
        return luau_vector
    elseif singleton == "Vector2" then
        return rbx_vector2
    elseif singleton == "Vector3" then
        return rbx_vector3
    elseif singleton == "UDim" then
        return rbx_udim
    elseif singleton == "UDim2" then
        return rbx_udim2
    elseif singleton == "BrickColor" then
        return rbx_brickcolor
    elseif singleton == "Color3" then
        return rbx_color3
    elseif singleton == "CFrame" then
        return rbx_cframe
    elseif singleton == "NumberSequence" then
        return rbx_numbersequence
    elseif singleton == "ColorSequence" then
        return rbx_colorsequence
    elseif singleton == "NumberRange" then
        return rbx_numberrange
    elseif singleton == "Rect" then
        return rbx_rect
    elseif singleton == "Font" then
        return rbx_font
    end
    error(
        `Unknown attribute type: {singleton}\nExpected string, number, boolean, vector, Vector2, Vector3, UDim, UDim2, BrickColor, Color3, CFrame, NumberSequence, ColorSequence, NumberRange, Rect or Font.`
    )
end

export type function ComponentInfo(t: type): type
    assert(t:is("table"), "`t` isn't a table")

    local props = t:properties()

    -- selene: allow(shadowing)
    local function optionalprop(
        props: { [type]: { read: type?, write: type? } },
        prop: type
    ): type?
        for key, rw in props do
            if key ~= prop then
                continue
            end

            local value = rw.read or rw.write
            if value then
                return value
            end
        end
        return nil
    end

    -- selene: allow(shadowing)
    local function fetchprop(
        props: { [type]: { read: type?, write: type? } },
        prop: type
    ): type
        local found = optionalprop(props, prop)
        assert(found ~= nil, `Prop not found: {prop:value()}`)
        return found
    end

    for key in props do
        local name = key:is("singleton") and key:value()
        assert(name ~= nil, `Found non singleton key: {key}`)
        assert(
            name == "is"
                or name == "tag"
                or name == "attributes"
                or name == "children",
            `Found an extra key: {name}`
        )
    end

    local is_prop = fetchprop(props, types.singleton("is"))
    assert(
        is_prop:is("singleton") and typeof(is_prop:value()) == "string",
        "`is` isn't a string singleton"
    )
    local tag_prop = fetchprop(props, types.singleton("tag"))
    assert(
        tag_prop:is("singleton") and typeof(tag_prop:value()) == "string",
        "`tag` isn't a string singleton"
    )

    local attributes_prop = optionalprop(props, types.singleton("attributes"))
    if attributes_prop then
        assert(attributes_prop:is("table"), "`attributes` isn't a table")
        assert(
            attributes_prop:indexer() == nil,
            "`attributes` shouldn't have an indexer but rather be a record"
        )
        for key, rw in attributes_prop:properties() do
            local attr_name = key:is("singleton") and key:value()
            assert(
                attr_name and typeof(attr_name) == "string",
                "Each key of `attributes` must be a string singleton"
            )
            local value = rw.read or rw.write
            assert(
                value ~= nil and value:is("table"),
                `Expected attribute {attr_name} to be a table with metadata about it`
            )

            local attr_props = value:properties()
            local kind_prop = fetchprop(attr_props, types.singleton("kind"))
            assert(
                kind_prop ~= nil and kind_prop:is("singleton"),
                `Expected attribute {attr_name} to have a \`kind\` key`
            )
            local expected_type: type = TypeOfKind(kind_prop)
            local default_prop =
                optionalprop(attr_props, types.singleton("default"))
            if default_prop then
                if default_prop.tag == "union" then
                    for _, component in default_prop:components() do
                        if component == types.singleton(nil) then
                            continue
                        end

                        default_prop = component
                        break
                    end
                end

                assert(
                    default_prop == expected_type,
                    `Unknown default type for attribute {attr_name}: expected {expected_type.tag} and got {default_prop.tag}`
                )
            end
        end
    end

    local children_prop = optionalprop(props, types.singleton("children"))
    if children_prop then
        assert(children_prop:is("table"), "`children` isn't a table")
        assert(
            children_prop:indexer() == nil,
            "`children` shouldn't have an indexer but rather be a record"
        )
        for key, rw in children_prop:properties() do
            local child_name = key:is("singleton") and key:value()
            assert(
                child_name and typeof(child_name) == "string",
                "Each key of `children` must be a string singleton"
            )
            local value = rw.read or rw.write
            assert(
                value ~= nil
                    and value:is("singleton")
                    and typeof(value:value()) == "string",
                `Expected child {child_name} to be a string singleton of it's class name`
            )
        end
    end

    return t
end

-- Luau vectors and Roblox Vector3s are interchangeable, although the types are absolute ass for it, so we define attribute kinds for both.
export type AttributeKind =
    | "string"
    | "number"
    | "boolean"
    | "vector"
    | "Vector2"
    | "Vector3"
    | "UDim"
    | "UDim2"
    | "BrickColor"
    | "Color3"
    | "CFrame"
    | "NumberSequence"
    | "ColorSequence"
    | "NumberRange"
    | "Rect"
    | "Font"
export type AttributeType =
    | string
    | number
    | boolean
    | vector
    | Vector2
    | Vector3
    | UDim
    | UDim2
    | BrickColor
    | Color3
    | CFrame
    | NumberSequence
    | ColorSequence
    | NumberRange
    | Rect
    | Font

export type AttributeMetadata = {
    kind: AttributeKind,
    default: AttributeType?,
}

export type ComponentInfoRepresentation = {
    is: string,
    tag: string,
    attributes: { [string]: AttributeMetadata }?,
    children: { [string]: string }?,
}

local function info_ctor<T>(info: T): ComponentInfo<T>
    return info
end

type function CheckKind(kind: type): type
    local singleton = kind:is("singleton") and kind:value()
    assert(singleton ~= nil, "The passed type isn't a singleton")
    assert(
        singleton == "string"
            or singleton == "number"
            or singleton == "boolean"
            or singleton == "vector"
            or singleton == "Vector2"
            or singleton == "Vector3"
            or singleton == "UDim"
            or singleton == "UDim2"
            or singleton == "BrickColor"
            or singleton == "Color3"
            or singleton == "CFrame"
            or singleton == "NumberSequence"
            or singleton == "ColorSequence"
            or singleton == "NumberRange"
            or singleton == "Rect"
            or singleton == "Font",
        `Unknown attribute type: {singleton}\nExpected string, number, boolean, vector, Vector2, Vector3, UDim, UDim2, BrickColor, Color3, CFrame, NumberSequence, ColorSequence, NumberRange, Rect or Font.`
    )
    return kind
end

local function attribute_ctor<T>(
    kind: T | AttributeKind,
    default: TypeOfKind<T>?
): { kind: CheckKind<T>, default: TypeOfKind<T>? }
    return {
        kind = kind :: any,
        default = default,
    }
end

local function child_ctor<T>(class_name: T | keyof<rblx_types.Instances> | ""): T
    return class_name
end

local function singleton_ctor<T>(x: T | ""): T
    return x
end

local constructors = table.freeze {
    info = info_ctor,
    attribute = attribute_ctor,
    child = child_ctor,
    singleton = singleton_ctor,
}

return constructors


--# selene: allow(undefined_variable)
local CollectionService = game:GetService("CollectionService")
local constructors = require("@self/constructors")
local core = require("@crates/core")
local rblx_types = require("@self/rblx_types")

export type ComponentInfo<T> = constructors.ComponentInfo<T>
type TypeOfKind<Kind> = constructors.TypeOfKind<Kind>
type GetInstance<ClassName> = rblx_types.GetInstance<
    ClassName,
    rblx_types.Instances
>
type function creation_data(t: type): type
    local _ = ComponentInfo(t)

    local function optionalprop(
        props: { [type]: { read: type?, write: type? } },
        prop: type
    ): type?
        for key, rw in props do
            if key ~= prop then
                continue
            end

            local value = rw.read or rw.write
            if value then
                return value
            end
        end
        return nil
    end

    local function fetchprop(
        props: { [type]: { read: type?, write: type? } },
        prop: type
    ): type
        local found = optionalprop(props, prop)
        assert(found ~= nil, `Prop not found: {prop:value()}`)
        return found :: any
    end

    local data = types.newtable()

    local props = t:properties()

    local attributes_prop = optionalprop(props, types.singleton("attributes"))
    if attributes_prop then
        local attributes = types.newtable()
        for key, rw in attributes_prop:properties() do
            local value = rw.read or rw.write
            if not value then
                continue
            end

            local kind = fetchprop(value:properties(), types.singleton("kind"))
            attributes:setreadproperty(key, TypeOfKind(kind))
        end
        data:setreadproperty(types.singleton("attributes"), attributes)
    end

    local children_prop = optionalprop(props, types.singleton("children"))
    if children_prop then
        local children = types.newtable()
        for key, rw in children_prop:properties() do
            local class_name = rw.read or rw.write
            if not class_name then
                continue
            end

            children:setreadproperty(key, GetInstance(class_name))
        end
        data:setreadproperty(types.singleton("children"), children)
    end

    local is_prop = fetchprop(props, types.singleton("is"))
    data:setreadproperty(types.singleton("instance"), GetInstance(is_prop))

    return data
end
export type CreationData<T> = creation_data<T> -- Linker doesn't support type functions

type CreationDataRepresentation = {
    instance: Instance,
    attributes: { [string]: constructors.AttributeType }?,
    children: { [string]: Instance }?,
}

type Component = {
    init: ((Component) -> ())?,
    destroy: ((Component) -> ())?,
    [any]: any,
}

local function fake_info_ctor<T>(info: T): ComponentInfo<T>
    return { UNINITIALIZED_COMPONENT = true, info = info } :: any
end

local function mod(debug: boolean?): core.Mod
    local function lifecycle_hook(
        lifecycle: core.Lifecycle,
        singleton: core.Singleton
    )
        if lifecycle ~= "initialized" then
            return
        end

        local meta: constructors.ComponentInfoRepresentation = singleton.info
        if
            not meta
            or type(meta) ~= "table"
            or not (meta :: any).UNINITIALIZED_COMPONENT
        then
            return
        end
        meta = (meta :: any).info :: constructors.ComponentInfoRepresentation
        singleton.info = meta

        local component_ctor: (core.Singleton, CreationDataRepresentation) -> Component =
            singleton.new
        if not component_ctor or type(component_ctor) ~= "function" then
            return
        end

        local setup_component
        if debug then
            function setup_component(instance: Instance)
                if not instance:IsA(meta.is) then
                    warn(
                        `Tried to set up an instance {instance} with tag {meta.tag} which is not a {meta.is}`
                    )
                    return
                end

                local data: CreationDataRepresentation = {
                    instance = instance,
                }

                if meta.children then
                    local children = {}
                    data.children = children

                    for name, class_name in meta.children do
                        local child = instance:FindFirstChild(name)
                        if not child then
                            warn(
                                `Tried to set up an instance {instance} with tag {meta.tag} which doesn't have a child {name}`
                            )
                            return
                        end

                        if not child:IsA(class_name) then
                            warn(
                                `Tried to set up an instance {instance} with tag {meta.tag} which's child {name} isn't a {class_name}`
                            )
                            return
                        end

                        children[name] = child
                    end
                end

                if meta.attributes then
                    local attributes = {}
                    data.attributes = attributes

                    for name, metadata in meta.attributes do
                        local value = instance:GetAttribute(name)
                        if not value then
                            if metadata.default ~= nil then
                                attributes[name] = value :: any
                                    or metadata.default
                                continue
                            end

                            warn(
                                `Tried to set up an instance {instance} with tag {meta.tag} which doesn't have an attribute {name}`
                            )
                            return
                        end

                        if typeof(value) ~= metadata.kind then
                            warn(
                                `Tried to set up an instance {instance} with tag {meta.tag} which's atribute {name} isn't of type {value}`
                            )
                            return
                        end

                        attributes[name] = value :: any
                    end
                end

                local component = component_ctor(singleton, data)
                if component.init then
                    component:init()
                end
                return component :: any
            end
        else
            function setup_component(instance: Instance)
                if not instance:IsA(meta.is) then
                    return
                end

                local data: CreationDataRepresentation = {
                    instance = instance,
                }

                if meta.children then
                    local children = {}
                    data.children = children

                    for name, class_name in meta.children do
                        local child = instance:FindFirstChild(name)
                        if not child or not child:IsA(class_name) then
                            return
                        end

                        children[name] = child
                    end
                end

                if meta.attributes then
                    local attributes = {}
                    data.attributes = attributes

                    for name, metadata in meta.attributes do
                        local value = instance:GetAttribute(name)
                        if not value or typeof(value) ~= metadata.kind then
                            if metadata.default ~= nil then
                                attributes[name] = value :: any
                                    or metadata.default
                                continue
                            end

                            return
                        end

                        attributes[name] = value :: any
                    end
                end

                local component = component_ctor(singleton, data)
                if component.init then
                    component:init()
                end
                return component :: any
            end
        end

        local dtors = {}

        local function handle_tagged(instance: Instance)
            local component = (setup_component :: (Instance) -> Component?)(
                instance
            )
            if component and component.destroy then
                dtors[instance] = function()
                    component:destroy()
                end
            end
        end

        CollectionService:GetInstanceAddedSignal(meta.tag)
            :Connect(handle_tagged)

        CollectionService:GetInstanceRemovedSignal(meta.tag)
            :Connect(function(instance)
                local dtor = dtors[instance]
                if dtor then
                    dtor()
                    dtors[instance] = nil
                end
            end)

        for _, instance in CollectionService:GetTagged(meta.tag) do
            handle_tagged(instance)
        end
    end

    return {
        on_lifecycle = lifecycle_hook,
    }
end

local components = table.freeze {
    mod = mod,

    info = fake_info_ctor,
    attribute = constructors.attribute,
    child = constructors.child,
    singleton = constructors.singleton,
}

return components


local CrateConfig = require("@mgr/specifiers/CrateConfig")

return CrateConfig {
    name = "rblx_lifecycles",
    description = "A collection of Quill lifecycles for Roblox",
    version = "0.1.0-alpha.1",
    lib = "lib",

    targets = {
        roblox = true,
    },

    workspace_dependencies = {
        core = true,
    },
}


local RunService = game:GetService("RunService")
local core = require("@crates/core")

local CLIENT_LIFECYCLES = {
    on_heartbeat = RunService.Heartbeat,
    on_post_simulation = RunService.PostSimulation,
    on_pre_animation = RunService.PreAnimation,
    on_pre_render = RunService.PreRender,
    on_pre_simulation = RunService.PreSimulation,
}
local SERVER_LIFECYCLES = {
    on_heartbeat = RunService.Heartbeat,
    on_post_simulation = RunService.PostSimulation,
    on_pre_animation = RunService.PreAnimation,
    on_pre_simulation = RunService.PreSimulation,
}

local function mod(): core.Mod
    local lifecycles_table = if RunService:IsServer()
        then SERVER_LIFECYCLES
        else CLIENT_LIFECYCLES

    local function lifecycle_hook(
        lifecycle: core.Lifecycle,
        singleton: core.Singleton
    )
        if lifecycle ~= "initialized" then
            return
        end

        for name, event: RBXScriptSignal in lifecycles_table do
            local fn = singleton[name]
            if not fn then
                continue
            end

            if typeof(fn) ~= "function" then
                warn(
                    `Tried registering lifecycle {name} for singleton {singleton}, got {typeof(
                        fn
                    )} instead of function`
                )
                continue
            end

            event:Connect(fn)
        end
    end

    return {
        on_lifecycle = lifecycle_hook,
    }
end

--- Implements a lifecycle for the client.\
--- Make sure this is called prior to modules being initialized.
local function implement_client_lifecycle(name: string, event: RBXScriptSignal)
    CLIENT_LIFECYCLES[name] = event
end

--- Implements a lifecycle for the server.\
--- Make sure this is called prior to modules being initialized.
local function implement_server_lifecycle(name: string, event: RBXScriptSignal)
    SERVER_LIFECYCLES[name] = event
end

local rblx_lifecycles = table.freeze {
    mod = mod,

    implement_client_lifecycle = implement_client_lifecycle,
    implement_server_lifecycle = implement_server_lifecycle,
}

return rblx_lifecycles


local fs = zune.fs
local stdpath = fs.path

export type FsFileNode = {
    name: string,
    kind: "file",
    parent: FsDirNode?,
}

export type FsDirNode = {
    name: string,
    kind: "dir",
    parent: FsDirNode?,
    children: { [string]: FsNode },
}

export type FsNode = FsFileNode | FsDirNode

local function build_fs_tree(path: string, parent: FsDirNode?): FsDirNode
    local children = {}
    local node = {
        name = stdpath.basename(path),
        kind = "dir",
        parent = parent,
        children = children,
    }

    for _, entry in fs.entries(path) do
        local child_path = stdpath.join(path, entry.name)
        local child_node: FsNode = if entry.kind == "directory"
            then build_fs_tree(child_path, node)
            else { name = entry.name, kind = "file", parent = node }
        children[entry.name] = child_node
    end

    return node
end

local function path_to_node(
    root: FsDirNode,
    path: string,
    traverse_modules: boolean?
): FsNode?
    local names = string.split(path, "/")
    assert(#names > 0, `Invalid path: {path}`)

    local node = root
    for idx, name in names do
        if name == "" then
            break
        end

        if name == "." then
            continue
        end

        if name == ".." then
            local parent = node.parent
            if not parent then
                return nil
            end
            node = node.parent
            continue
        end

        local child = node.children[name]

        if not child and traverse_modules then
            child = node.children[name .. ".luau"]
            if not child then
                child = node.children["init.luau"]
                if not child then
                    return nil
                end
            end
        end

        if child.kind == "file" and idx < #names then
            return nil
        end

        node = child
    end

    if traverse_modules and node.kind == "dir" then
        local possible_module = node.children["init.luau"]
        if possible_module then
            return possible_module
        end
    end

    return node
end

local function get_closest_common_ancestor(from: FsNode, to: FsNode): FsNode?
    local from_ancestors: { FsDirNode } = {}
    do
        local ancestor = from.parent
        while ancestor ~= nil do
            table.insert(from_ancestors, ancestor)
            ancestor = ancestor.parent
        end

        if #from_ancestors <= 0 then
            return nil
        end
    end

    local to_ancestor_lookup: { [FsDirNode]: true } = {}
    local to_ancestors: { FsDirNode } = {}
    do
        local ancestor = to.parent
        while ancestor ~= nil do
            to_ancestor_lookup[ancestor] = true
            table.insert(to_ancestors, ancestor)
            ancestor = ancestor.parent
        end

        if #to_ancestors <= 0 then
            return nil
        end
    end

    local closest_common_ancestor: FsDirNode? = nil
    for _, ancestor in from_ancestors do
        if to_ancestor_lookup[ancestor] then
            closest_common_ancestor = ancestor
            break
        end
    end

    return closest_common_ancestor
end

local function get_relative_module_path(
    from: FsFileNode,
    to: FsFileNode
): string?
    local common_ancestor = get_closest_common_ancestor(from, to)
    if not common_ancestor then
        return nil
    end

    local path = ""
    if not from.parent then
        return nil
    end

    local traverse_from: FsNode = nil :: any
    local is_init = stdpath.stem(from.name) == "init"
    if common_ancestor == from.parent then
        path = if is_init then "@self" else "."
        traverse_from = from.parent :: FsNode
    else
        if not from.parent.parent then
            return nil
        end

        if common_ancestor == from.parent.parent then
            path = if is_init then "." else ".."
            traverse_from = from.parent.parent :: FsNode
        else
            if not from.parent.parent or not from.parent.parent.parent then
                return nil
            end

            path = if is_init then ".." else "../.."
            traverse_from = from.parent.parent.parent :: FsNode
        end
    end

    local from_ancestor = traverse_from.parent :: FsNode?
    while
        from_ancestor ~= nil
        and from_ancestor ~= common_ancestor
        and from_ancestor ~= common_ancestor.parent
    do
        path ..= "/.."
        from_ancestor = from_ancestor.parent
    end

    local to_ancestor = to.parent
    local to_stem = stdpath.stem(to.name)
    if to_stem == "init" then
        to_ancestor = assert(to_ancestor).parent
    end

    local to_ancestors: { FsNode } = {}
    while to_ancestor ~= nil and to_ancestor ~= common_ancestor do
        table.insert(to_ancestors, to_ancestor)
        to_ancestor = to_ancestor.parent
    end
    for idx = #to_ancestors, 1, -1 do
        local descendant = to_ancestors[idx]
        path ..= `/{descendant.name}`
    end

    local is_to_init = stdpath.stem(to.name) == "init"
    path ..= if is_to_init then `/{assert(to.parent).name}` else `/{to_stem}`

    return path
end

local function build_resolver(
    fs_root: FsDirNode,
    aliases: { [string]: string }
): (path: string, from: string) -> string
    local alias_nodes = {}
    for alias, path in aliases do
        local node = path_to_node(fs_root, path)
        if not node then
            error(
                `Alias {alias} with path {path} cannot be resolved into a node`
            )
        end
        alias_nodes[alias] = node
    end

    return function(require_path: string, from: string): string
        local alias = string.match(require_path, "^@([^/]+)")
        if not alias or not aliases[alias] then
            error(`Unresolvable alias: {alias}`)
        end
        local alias_node = alias_nodes[alias]

        local origin_node = path_to_node(fs_root, from, true)
        if not origin_node or origin_node.kind ~= "file" then
            error(`The path {from} cannot be resolved into a node`)
        end

        local target_node: FsNode = alias_node
        local first_slash_idx = string.find(require_path, "/")
        if first_slash_idx then
            local len = #require_path
            local target_path =
                string.sub(require_path, first_slash_idx + 1, len)
            if #target_path + 1 ~= len then
                assert(alias_node.kind == "dir")
                target_node = assert(
                    path_to_node(alias_node, target_path, true),
                    `The path {require_path} cannot be resolved into a node`
                )
            elseif alias_node.kind == "dir" then
                target_node = assert(
                    path_to_node(alias_node, "init.luau", true),
                    `The path {require_path} cannot be resolved into a node`
                )
            end
        elseif alias_node.kind == "dir" then
            target_node = assert(
                path_to_node(alias_node, "init.luau", true),
                `The path {require_path} cannot be resolved into a node`
            )
        end
        assert(target_node.kind == "file")

        local path = get_relative_module_path(origin_node, target_node)
        assert(path, `Failed to navigate from {from} to {require_path}`)
        return path
    end
end

return {
    build_fs_tree = build_fs_tree,
    build_resolver = build_resolver,
}


local fs = zune.fs
local stdpath = fs.path
local toml = zune.serde.toml
local CrateConfig = require("@mgr/specifiers/CrateConfig")
local RootConfig = require("@mgr/specifiers/RootConfig")
local alias_resolver = require("@mgr/build/alias_resolver")
local ansi = require("@mgr/util/ansi")
local dynrequire = require("@mgr/dynrequire")
local fs_util = require("../../util/fs")

export type DependencySpecifier = | { name: string, version: string }

export type BuiltManifest = {
    name: string,
    version: string,
    description: string,
    license: string,
    authors: { string },
    repository: string,
    includes: { string },

    target: {
        environment: string,
        lib: string,
        build_files: { string }?,
    },

    indices: {
        default: string,
    },

    dependencies: {
        [string]: DependencySpecifier,
    },
}

local function build(
    dir: string,
    crates: { string },
    targets: { string },
    output: string,
    root_config: RootConfig.RootConfig
)
    local pesde_config = root_config.package_managers.pesde
    if not pesde_config then
        error(
            `Aborting. A pesde config doesn't exist in the root configuration.`
        )
    end
    local root_name = pesde_config.root_name or root_config.name

    if not fs_util.exists(dir) then
        error(`Aborting build. The directory {dir} doesn't exist.`)
    end

    if not fs_util.exists(output) then
        fs.makeDir(output)
    end

    for _, crate in crates do
        local crate_path = stdpath.join(dir, crate)
        local config_path = stdpath.join(crate_path, "crate.luau")
        if not fs_util.exists(config_path) then
            error(
                `Aborting build. The crate {crate} @ path {crate_path} doesn't have a config @ path {config_path}.`
            )
        end

        local config = dynrequire(config_path) :: CrateConfig.CrateConfig
        local lib_path = stdpath.join(crate_path, config.lib)
        local output_tree: alias_resolver.FsDirNode = {
            name = "root",
            kind = "dir",
            children = {},
        }
        output_tree.children = {
            lib = alias_resolver.build_fs_tree(lib_path, output_tree),
            crates = alias_resolver.build_fs_tree(dir, output_tree),
        }

        local found_target = false
        for crate_target in config.targets do
            if not table.find(targets, crate_target) then
                continue
            end
            found_target = true

            print(
                `{ansi.bg.yellow}{ansi.black}{ansi.bold}Building{ansi.reset} pesde crate {ansi.cyan}{crate}{ansi.reset} of target {ansi.red}{crate_target}{ansi.reset}`
            )

            local output_path =
                stdpath.join(output, "pesde", crate_target, crate)
            local output_lib_path = stdpath.join(output_path, config.lib)
            if fs_util.exists(output_path) then
                fs.deleteDir(output_path, true)
            end
            fs.makeDir(output_path, true)
            fs.copy(lib_path, output_lib_path)

            local aliases = { crates = "crates" }
            local pkg_path = `{crate_target}_packages`
            if fs_util.exists(pkg_path) then
                output_tree.children[pkg_path] =
                    alias_resolver.build_fs_tree(pkg_path, output_tree)
                aliases.pkg = pkg_path
            end

            local resolve = alias_resolver.build_resolver(output_tree, aliases)

            fs_util.traverse(
                output_lib_path,
                function(path, name, metadata, local_path)
                    if
                        metadata.kind ~= "file"
                        or stdpath.extension(path) ~= ".luau"
                    then
                        return
                    end
                    local_path = string.gsub(local_path, "\\", "/")

                    local contents = fs.readFile(path)
                    local lines = string.split(contents, "\n")
                    for idx, line in lines do
                        local require_path =
                            string.match(line, "require%(\"(@[^\"]+)\"%)")
                        if not require_path then
                            continue
                        end

                        local alias = string.match(require_path, "^@([^/]+)")
                        if not alias or alias == "self" then
                            continue
                        end

                        if alias ~= "pkg" and alias ~= "crates" then
                            print(
                                `Found unresolvable alias (not @pkg or @crates) in file {path} at line {idx}:\n{line}`
                            )
                            continue
                        end

                        local resolved_path = resolve(require_path, local_path)
                        local resolved =
                            string.gsub(resolved_path, "crates", pkg_path, 1)
                        local resolved_line =
                            string.gsub(line, require_path, resolved)
                        lines[idx] = resolved_line
                    end

                    -- Dumbass windows behavior patch, replace in zune 0.6.0 with handles end-to-end
                    local handle = fs.createFile(path, { truncate = true })
                    handle:write(table.concat(lines, "\n"))
                    handle:sync()
                    handle:close()
                end
            )

            local dependencies: { [string]: any } = {}
            local has_dependencies = false
            if config.dependencies then
                for alias, dependency in config.dependencies do
                    has_dependencies = true
                    dependencies[alias] = dependency.pesde
                end
            end
            if config.workspace_dependencies then
                for crate_name in config.workspace_dependencies do
                    has_dependencies = true
                    local crate_config_path =
                        stdpath.join(dir, crate_name, "crate.luau")
                    local crate_config =
                        dynrequire(crate_config_path) :: CrateConfig.CrateConfig
                    dependencies[crate_name] = {
                        name = `{pesde_config.scope}/{root_name}_{crate_config.name}`,
                        version = crate_config.version,
                    }
                end
            end

            local manifest: BuiltManifest = {
                name = `{pesde_config.scope}/{root_name}_{config.name}`,
                version = config.version,
                description = config.description,
                license = root_config.license,
                authors = root_config.authors,
                repository = root_config.repository,
                includes = {
                    config.lib,
                    `{config.lib}/**`,
                    "LICENSE",
                    "pesde.toml",
                    "README.md",
                },

                target = {
                    environment = crate_target,
                    lib = `{config.lib}/init.luau`,
                    build_files = if crate_target == "roblox"
                        then { config.lib }
                        else nil,
                },

                indices = {
                    default = pesde_config.registry,
                },

                dependencies = dependencies,
            }

            if not has_dependencies then
                manifest.dependencies = nil :: any
            end

            local encoded_manifest = toml.encode(manifest :: any)
            fs.writeFile(
                stdpath.join(output_path, "pesde.toml"),
                encoded_manifest
            )
            fs.copy("LICENSE", stdpath.join(output_path, "LICENSE"))
            fs.copy(
                stdpath.join(crate_path, "README.md"),
                stdpath.join(output_path, "README.md")
            )
        end

        if not found_target then
            warn(
                `Encountered an error, but continuing - the crate {crate} doesn't support any listed targets.`
            )
            continue
        end
    end
end

return build


local RootConfig = require("@mgr/specifiers/RootConfig")
local build_pesde = require("@self/builders/pesde")
local build_wally = require("@self/builders/wally")

local function build(options: {
    dir: string,
    crates: { string },
    package_managers: { string },
    targets: { string },
    output: string,
    root_config: RootConfig.RootConfig,
})
    for _, package_manager in options.package_managers do
        if package_manager == "pesde" then
            build_pesde(
                options.dir,
                options.crates,
                options.targets,
                options.output,
                options.root_config
            )
        elseif package_manager == "wally" then
            build_wally(
                options.dir,
                options.crates,
                options.targets,
                options.output,
                options.root_config
            )
        else
            error(`Invalid package manager: {package_manager}`)
        end
    end
end

return build


local luau = zune.luau
local fs = zune.fs

local cache = {}
local function dynrequire<T>(path: string): T
    local hit = cache[path]
    if hit then
        return hit :: T
    end

    local contents = fs.readFile(path)
    local bytecode = luau.compile(contents, { optimization_level = 2 })
    local result = luau.load(bytecode)() :: any
    cache[path] = result
    return result :: T
end

return dynrequire


local CrateConfig = require("@mgr/specifiers/CrateConfig")
local RootConfig = require("@mgr/specifiers/RootConfig")
local dynrequire = require("@self/dynrequire")
local frkcli = require("@self/vendor/frkcli")
local process = zune.process
local fs = zune.fs
local fs_util = require("@self/util/fs")

local root_config = require("./root")

local available_crates = {}
local available_targets = {}
for _, entry in fs.entries(root_config.crates) do
    if entry.kind ~= "directory" then
        continue
    end

    local path = fs.path.join(root_config.crates, entry.name, "crate.luau")
    if not fs_util.exists(path) then
        continue
    end

    local crate_config = dynrequire(path) :: CrateConfig.CrateConfig
    for target in crate_config.targets do
        if not table.find(available_targets, target) then
            table.insert(available_targets, target)
        end
    end

    table.insert(available_crates, entry.name)
end
local available_crates_help = "all," .. table.concat(available_crates, ",")
local available_targets_help = "all," .. table.concat(available_targets, ",")

local available_package_managers = {}
for name in
    root_config.package_managers :: { [string]: RootConfig.PackageManagerConfig }
do
    table.insert(available_package_managers, name)
end
local available_package_managers_help = "all,"
    .. table.concat(available_package_managers, ",")

local cli = frkcli.new_subcommands("mgr", "A CLI to manage the Quill monorepo")

local cmd_check = cli:add_subcommand("check", "Lints and tests the code")
cmd_check:add_positional("source", {
    help = "The source folder / file to check",
    default = root_config.crates,
    aliases = { "-s" },
})

local cmd_link = cli:add_subcommand("link", "Creates a linker file for a crate")
cmd_link:add_positional("crates", {
    help = `The crates to create a linker for. Available: {available_crates_help}`,
    default = "all",
    aliases = { "-c" },
})

local cmd_build =
    cli:add_subcommand("build", "Builds the crates into distributable packages")
cmd_build:add_option("crates", {
    help = `A comma separated list of crates to build. Available: {available_crates_help}`,
    default = "all",
    aliases = { "-c" },
})
cmd_build:add_option("package-managers", {
    help = `A comma separated list of package managers to build for. Available: {available_package_managers_help}`,
    default = "all",
    aliases = { "-p" },
})
cmd_build:add_option("targets", {
    help = "A comma separated list of targets to build for, if available on a crate. Available: all,luau,roblox",
    default = "all",
    aliases = { "-t" },
})
cmd_build:add_option("output", {
    help = "The folder to output the built crates into",
    default = "out/",
    aliases = { "-o" },
})

local cmd_publish = cli:add_subcommand(
    "publish",
    "Publishes the crates to all supported package managers"
)
cmd_publish:add_option("crates", {
    help = `A comma separated list of crates to build. Available: {available_crates_help}`,
    default = "all",
    aliases = { "-c" },
})
cmd_publish:add_option("package-managers", {
    help = `A comma separated list of package managers to publish. Available: {available_package_managers_help}`,
    default = "all",
    aliases = { "-p" },
})
cmd_publish:add_option("targets", {
    help = `A comma separated list of targets to build for, if available on a crate. Available: {available_targets_help}`,
    default = "all",
    aliases = { "-t" },
})
cmd_publish:add_option("storage", {
    help = "The folder the built crates reside in",
    default = "out/",
    aliases = { "-s" },
})
cmd_publish:add_flag("build", {
    help = "Whether the crates should be built prior to publishing",
    aliases = { "-b" },
})
cmd_publish:add_flag("dry", {
    help = "Whether the crates should be dry published (packaged) instead of publishing",
    aliases = { "-d" },
})

local process_args = process.args
table.remove(process_args, 1)

local parsed, err = cli:parse(process_args)
if err ~= nil then
    error(err)
end
assert(parsed)

local function parse_crates(str: string): { string }
    local crates = string.split(str, ",")
    if table.find(crates, "all") then
        crates = available_crates
    else
        for _, crate in crates do
            if not table.find(available_crates, crate) then
                error(
                    `The provided crate {crate} is not available / does not exist.`
                )
            end
        end
    end

    if #crates <= 0 then
        error("Quitting. None of the provided crates are available.")
    end

    return crates
end

local values = parsed.result.values
local flags = parsed.result.flags
if parsed.command == "check" then
    require("@self/check") {
        source = values.source,
    }
elseif parsed.command == "link" then
    local crates = parse_crates(values.crates)
    for _, crate in crates do
        local path = fs.path.join(root_config.crates, crate)
        require("@self/link") {
            crate_path = path,
        }
    end
elseif parsed.command == "build" or parsed.command == "publish" then
    local crates = parse_crates(values.crates)

    local package_managers = string.split(values["package-managers"], ",")
    if table.find(package_managers, "all") then
        package_managers = available_package_managers
    else
        for _, package_manager in package_managers do
            if not table.find(available_package_managers, package_manager) then
                error(
                    `The provided package manager {package_manager} is not available. Check your root configuration.`
                )
            end
        end
    end

    if #package_managers <= 0 then
        local msg = if parsed.command == "publish"
            then "Quitting. The number of package managers to publish to is 0."
            else "Quitting. The number of package managers to build for is 0."
        warn(msg)
    end

    local targets = string.split(values.targets, ",")
    if table.find(targets, "all") then
        targets = available_targets
    else
        for _, target in targets do
            if not table.find(available_targets, target) then
                error(
                    `The provided target {target} is not available. Check your crate configurations.`
                )
            end
        end
    end

    if flags.build or parsed.command == "build" then
        local output = if parsed.command == "build"
            then values.output
            else values.storage

        require("@self/build") {
            dir = root_config.crates,
            crates = crates,
            package_managers = package_managers,
            targets = targets,
            output = output,
            root_config = root_config,
        }
    end

    if parsed.command ~= "publish" then
        return
    end

    require("@self/publish") {
        dir = root_config.crates,
        crates = crates,
        package_managers = package_managers,
        targets = targets,
        output = values.storage,
        dry = flags.dry,
    }
end


local fs = zune.fs
local stdpath = fs.path
local CrateConfig = require("@mgr/specifiers/CrateConfig")
local Process = require("./util/Process")
local ansi = require("./util/ansi")
local dynrequire = require("./dynrequire")

type Array<T> = { T }
type Map<K, V> = { [K]: V }
type Graph<T> = Map<T, Array<T>>
-- Based on Kahn's algorithm
local function toposort<T>(graph: Graph<T>): Array<T>
    local in_degree: Map<T, number> = {}
    for node, neighbors in graph do
        in_degree[node] = 0

        for _, neighbor in neighbors do
            if not in_degree[neighbor] then
                in_degree[neighbor] = 0
            end
            (in_degree :: any)[neighbor] += 1
        end
    end

    local queue: Array<T> = {}
    for node, degree in in_degree do
        if degree == 0 then
            table.insert(queue, node)
        end
    end

    local sorted_order: Array<T> = {}
    while #queue > 0 do
        local node = table.remove(queue, 1)
        table.insert(sorted_order, node)
        for _, neighbor: T in graph[node] or {} do
            local degree = in_degree[neighbor] - 1
            in_degree[neighbor] = degree
            if degree == 0 then
                table.insert(queue, neighbor)
            end
        end
    end

    return sorted_order
end

local function publish(options: {
    dir: string,
    crates: { string },
    package_managers: { string },
    targets: { string },
    output: string,
    dry: boolean,
})
    print(`{ansi.bg.magenta}{ansi.black}{ansi.bold}Ordering{ansi.reset} crates`)
    local dependency_graph = {}
    for _, crate in options.crates do
        local crate_config_path = stdpath.join(options.dir, crate, "crate.luau")
        local crate_config: CrateConfig.CrateConfig =
            dynrequire(crate_config_path) :: any

        local crate_dependencies = {}
        if crate_config.workspace_dependencies then
            for dependency in crate_config.workspace_dependencies do
                if not table.find(options.crates, dependency) then
                    continue
                end

                table.insert(crate_dependencies, dependency)
            end
        end

        dependency_graph[crate] = crate_dependencies
    end

    local ordered = toposort(dependency_graph)

    for _, package_manager in options.package_managers do
        if package_manager == "wally" then
            local sub_command = if options.dry then "package" else "publish"
            local process =
                Process.new("wally"):argument(sub_command):inherit_stdout()
            if options.dry then
                process:argument("--output"):argument("package.tar.gz")
            end

            for idx = #ordered, 1, -1 do
                local crate = ordered[idx]
                local out_dir =
                    stdpath.join(options.output, package_manager, crate)
                print(
                    `{ansi.bg.yellow}{ansi.black}{ansi.bold}Publishing{ansi.reset} wally crate {ansi.cyan}{crate}{ansi.reset}`
                )
                process:option("cwd", out_dir):run()
            end

            continue
        end

        if package_manager ~= "pesde" then
            warn(`Unsupported package manager: {package_manager}`)
            continue
        end

        local process = Process.new("pesde")
            :argument("publish")
            :argument("--yes")
            :inherit_stdout()
        if options.dry then
            process:argument("--dry-run")
        end

        local install_process =
            Process.new("pesde"):argument("install"):inherit_stdout()

        for _, target in options.targets do
            for idx = #ordered, 1, -1 do
                local crate = ordered[idx]
                local out_dir =
                    stdpath.join(options.output, package_manager, target, crate)

                if fs.stat(out_dir).kind == "none" then
                    -- Not built, likely because it's not available for this target
                    continue
                end

                if target == "roblox" then
                    print(
                        `{ansi.bg.blue}{ansi.black}{ansi.bold}Updating{ansi.reset} lockfile for pesde crate {ansi.cyan}{crate}{ansi.reset} of target {ansi.red}{target}{ansi.reset}`
                    )
                    install_process:option("cwd", out_dir):run()
                end

                print(
                    `{ansi.bg.yellow}{ansi.black}{ansi.bold}Publishing{ansi.reset} pesde crate {ansi.cyan}{crate}{ansi.reset} to target {ansi.red}{target}{ansi.reset}`
                )
                process:option("cwd", out_dir):run()
            end
        end
    end
end

return publish


export type WorkspaceCrate =
    | "core"
    | "dependencies"
    | "logger"
    | "rblx_components"
    | "rblx_lifecycles"

export type PesdePackageVariant = {
    name: string,
    version: string,
}

export type WallyPackageVariant = {
    name: string,
    version: string,
}

export type DependencySpecifier = {
    pesde: PesdePackageVariant,
    wally: WallyPackageVariant,
}

export type Target = "luau" | "roblox"

export type CrateConfig = {
    name: WorkspaceCrate,
    description: string,
    version: string,
    lib: string,

    targets: { [Target]: true },
    workspace_dependencies: { [WorkspaceCrate]: true }?,
    dependencies: { [string]: DependencySpecifier }?,
}

local function Type(config: CrateConfig): CrateConfig
    return config
end

return Type


export type PackageManagerConfig = {
    scope: string,
    --- `{scope}/{root_name}_{crate_name}`, defaults to `RootConfig.name`
    root_name: string?,
    registry: string,
}

export type RootConfig = {
    name: string,
    crates: string,

    repository: string,
    homepage: string,
    authors: { string },
    license: string,

    package_managers: {
        pesde: PackageManagerConfig?,
        wally: PackageManagerConfig?,
    },
}

local function Type(config: RootConfig): RootConfig
    return config
end

return Type


local process = zune.process
local task = zune.task

local Process = {}
local mt = { __index = Process }

type Properties = {
    command: string,
    args: { string },
    options: ProcessOptions,
}
export type Identity = setmetatable<Properties, typeof(mt)>

local function constructor(command: string): Identity
    local self: Properties = {
        command = command,
        args = {},
        options = {},
    }

    return setmetatable(self, mt)
end

function Process.argument(self: Identity, argument: string): Identity
    table.insert(self.args, argument)
    return self
end

function Process.option(
    self: Identity,
    option: keyof<ProcessOptions>,
    value: index<ProcessOptions, keyof<ProcessOptions>>
): Identity
    (self.options :: any)[option] = value
    return self
end

function Process.create(self: Identity): ProcessChild
    return process.create(self.command, self.args, self.options)
end

function Process.run<T>(self: Identity, carry_exit: boolean?): ProcessRunResult<T>
    local result = process.run(self.command, self.args, self.options)
    if not result.ok and (carry_exit == nil or carry_exit == true) then
        error(result.code)
    end
    return result
end

function Process.inherit_stdout(self: Identity): Identity
    return self:option("stdout", "inherit"):option("stderr", "inherit")
end

function Process.spawn(self: Identity): thread
    return task.spawn(process.run :: any, self.command, self.args, self.options)
end

return {
    new = constructor,
}


local fs = zune.fs
local stdpath = fs.path
local task = zune.task
local process = zune.process

local function exists(path: string): boolean
    return fs.stat(path).kind ~= "none"
end

local function is_dir(path: string): boolean
    return fs.stat(path).kind == "directory"
end

local function ensure_dirs(dirs: { string })
    for _, dir in dirs do
        if exists(dir) and is_dir(dir) then
            continue
        end
        fs.makeDir(dir, true)
    end
end

local function remove(path: string, recursive: boolean?)
    if is_dir(path) then
        fs.deleteDir(path, recursive)
        return
    end
    fs.deleteFile(path)
end

local function remove_safe(path: string, recursive: boolean?)
    if not exists(path) then
        return
    end
    remove(path, recursive)
end

local function shell_remove(
    path: string,
    flags: { recursive: boolean?, force: boolean? }?
)
    local args = table.create(3) :: { string }
    if flags then
        if flags.recursive then
            table.insert(args, "-r")
        end

        if flags.force then
            table.insert(args, "-Force")
        end
    end
    table.insert(args, path)
    process.run("rm", args, { shell = "pwsh" } :: any)
end

local function mime_folders(paths: { string }): () -> ()
    local acting: { [string]: true } = {}
    for _, path in paths do
        if is_dir(path) then
            continue
        end
        fs.deleteDir(path)
        acting[path] = true
    end

    local function cleanup()
        for path in acting do
            fs.deleteDir(path)
        end
    end

    return cleanup
end

local function traverse(
    dir: string,
    fn: (
        path: string,
        name: string,
        metadata: Metadata,
        local_path: string
    ) -> (),
    start_name: string?
)
    assert(is_dir(dir))

    local local_name = start_name or stdpath.basename(dir)

    for _, entry in fs.entries(dir) do
        local path = stdpath.join(dir, entry.name)
        local child = fs.metadata(path)
        if not child then
            continue
        end

        local local_path = stdpath.join(local_name, entry.name)
        fn(path, entry.name, child, local_path)
        if child.kind == "directory" then
            traverse(path, fn, local_path)
            continue
        end
    end
end

local function watch_file(
    path: string,
    fn: (metadata: Metadata) -> ()
): () -> ()
    local origin_metadata = fs.metadata(path)
    local last_modified = origin_metadata.modified_at

    local thread = task.spawn(function()
        while true do
            task.wait(1)

            local metadata = fs.metadata(path)
            if last_modified < metadata.modified_at then
                last_modified = metadata.modified_at
                fn(metadata)
            end
        end
    end)

    return function()
        task.cancel(thread)
    end
end

local function relative_require_path(from: string, to: string)
    return string.gsub(stdpath.relative(from, to), stdpath.stem(to), "")
end

return {
    exists = exists,
    ensure_dirs = ensure_dirs,
    remove = remove,
    remove_safe = remove_safe,
    shell_remove = shell_remove,
    mime_folders = mime_folders,
    traverse = traverse,
    watch_file = watch_file,
    relative_require_path = relative_require_path,
}


local luau = zune.luau
local ast_types = require("./types")

local function parseBlock(source: string | buffer): ast_types.AstStatBlock
    return (luau.parse(source) :: any).root
end

local function parseExpr(source: string | buffer): ast_types.AstExpr
    return luau.parseExpr(source)
end

local function parse(source: string | buffer): ast_types.ParseResult
    return luau.parse(source) :: any
end

local parser = table.freeze {
    parseBlock = parseBlock,
    parseExpr = parseExpr,
    parse = parse,
}

return parser


export type function extend(base: type, superset: type): type
    assert(base:is("table") and superset:is("table"))

    local extended = types.newtable()

    for key, value in base:properties() do
        if value.read and value.write then
            extended:setproperty(key, value.read)
            continue
        end

        if value.read then
            extended:setreadproperty(key, value.read)
        elseif value.write then
            extended:setwriteproperty(key, value.write)
        end
    end

    for key, value in superset:properties() do
        if value.read and value.write then
            extended:setproperty(key, value.read)
            continue
        end

        if value.read then
            extended:setreadproperty(key, value.read)
        elseif value.write then
            extended:setwriteproperty(key, value.write)
        end
    end

    return extended
end

return {}


return [==[--// Types are duplicates from `RuntimeTypes.luau`
type Solder = {
    --[[
        Invokes the real `require` function, instead of the virtualized one.
    ]]
    require: typeof(require),

    --[[
        The environment name that will be emitted in unhandled errors.

        Example: `<SolderRuntime> • (SomeFolder.Handler): An unexpected error occured.`, where `SolderRuntime` is the environment name.
    ]]
    environmentName: string,

    --[[
        The "real script", aka the `script` global in the current Luau environment.
    ]]
    script: LuaSourceContainer,

    --[[
        Reports whether line tracking was enabled for this build.
    ]]
    lineTrackingEnabled: boolean,

    --[[
        Gets the "real line", aka the line that would be in the original file.

        Example: A module calls `solder.getRealLine` and passes line 7100, and the module starts at line 7000 in the bundled file, then it would return 100.
    ]]
    getRealLine: (line: number) -> number,
}

type SolderClassNames = "Folder" | "Script" | "LocalScript" | "ModuleScript"
type ISolderInstance = {
    ClassName: SolderClassNames,
    Parent: (Instance | SolderInstance)?,
    Name: string,

    _Children: {SolderInstance}
}

type SolderInstanceImpl = {
    __index: SolderInstanceImpl,
    new: (name: string, className: SolderClassNames, parent: typeof(({} :: ISolderInstance).Parent), childrenSize: number?) -> SolderInstance,

    GetFullName: (self: SolderInstance) -> string,
    FindFirstChild: (self: SolderInstance, name: string, recursive: boolean?) -> SolderInstance?,
    WaitForChild: (self: SolderInstance, name: string, timeout: number?) -> SolderInstance?, --// Note that timeout is ignored.
    GetChildren: (self: SolderInstance) -> typeof(({} :: ISolderInstance)._Children)
}

type SolderInstance = typeof(setmetatable({} :: ISolderInstance, {} :: SolderInstanceImpl))

type SolderInitClosure = (require: typeof(require), script: (SolderInstance & LuaSourceContainer)?) -> ((...any?) -> ...any?)

local closures: {SolderInitClosure} = {
--MARKER:CLOSURES
}

local lineOffsets: {number} = {
--MARKER:TRACKEDLINES
}

local chunkNames: {string} = {
--MARKER:CHUNKNAMES
}

local SolderInstance = {} :: SolderInstanceImpl
SolderInstance.__index = SolderInstance --// For typechecking
rawset(SolderInstance, "__index", function(self: SolderInstance, index: string): any | SolderInstance
    local solderMethod = SolderInstance[index]
    if solderMethod then
        return solderMethod
    end

    local child = self:FindFirstChild(index)
    if (not child) and index ~= "Parent" then
        error(`{index} is not a valid member of {self.ClassName} "{self:GetFullName()}"`, 2)
    end

    return child
end)

function SolderInstance.new(name, className, parent, childrenSize)
    local self = setmetatable({
        Name = name,
        ClassName = className,
        Parent = parent,
        _Children = table.create(childrenSize or 0),
    } :: ISolderInstance, SolderInstance)

    if parent and typeof(parent) ~= "Instance" then
        table.insert((parent :: SolderInstance)._Children, self)
    end

    return self
end

function SolderInstance:GetFullName()
    local fullName = self.Name

    local parent = self.Parent
    while parent do
        fullName = `{parent.Name}.{fullName}`
        parent = parent.Parent
    end

    return fullName
end

function SolderInstance:FindFirstChild(name, recursive)
    for _, child in self._Children do
        if child.Name == name then
            return child
        end

        if not recursive then
            continue
        end

        local potentialChild = child:FindFirstChild(name, recursive)
        if potentialChild then
            return potentialChild
        end
    end

    return nil
end

function SolderInstance:WaitForChild(name, timeout)
    --// Timeout is ignored
    return self:FindFirstChild(name)
end

function SolderInstance:GetChildren()
    return self._Children
end

local scripts: {[number]: SolderInstance} = {}
--MARKER:INSTANCES

local entryPoints: {number} = {
--MARKER:ENTRYPOINTS
}

local solder = {} :: Solder

solder.require = require
solder.environmentName = [[--MARKER:ENVNAME]]
solder.script = script
solder.lineTrackingEnabled = #lineOffsets ~= 0

local lineCache: {number} = {}
solder.getRealLine = function(line)
    if not solder.lineTrackingEnabled then
        error("solder::getRealLine is not available because line tracking is disabled.", 2)
    end

    if lineCache[line] then
        return lineCache[line]
    end

    local closest: number = lineOffsets[1]
    local closestDifference = math.huge

    for _, lineOffset in lineOffsets do
        if lineOffset - 1 < line then
            local difference = line - lineOffset
            if difference < closestDifference then
                closestDifference = difference
                closest = lineOffset
            end
        end
    end

    local realLine = (line - closest) + 1
    lineCache[line] = realLine

    return realLine
end

local constructorDebugLine = debug.info(SolderInstance.new, "l") :: number
local function buildErrorTree(closureId: number, message: string): string
    local treeBranchMiddle, treeBranchEnd = "├", "└"

    local errorLine = debug.info(3, "l")
    local errorDisplayLine = errorLine ~= -1 and `:{solder.getRealLine(errorLine)}` or ""
    local displayMessage = `<{solder.environmentName}> • ({chunkNames[closureId] or "?"}{errorDisplayLine}): \`{message}\``

    for level = 4, math.huge do
        local line = debug.info(level, "l")
        local nextLine = debug.info(level + 1, "l")
        if (not line) then
            break
        end

        if line == -1 or line >= constructorDebugLine then
            continue --// skip C closures & internal closures
        end

        local displayLine = solder.getRealLine(line)
        local branchCharacter = (nextLine and nextLine ~= -1) and treeBranchMiddle or treeBranchEnd
        displayMessage ..= `\n\t{branchCharacter} '{chunkNames[closureId] or "?"}', line {displayLine}`
    end

    return displayMessage
end

local cachedRequires: {[SolderInstance]: any} = {}
local function errorIfInvalidRet(ret: {[number]: any, n: number}): ()
    if ret.n ~= 2 then --// adjust for pcall result
        error("Module code did not return exactly one value", 4)
    end
end

local function getClosureFromSolderInstance(target: SolderInstance): (SolderInitClosure, number)
    local closureId = table.find(scripts, target) :: number
    return closures[closureId], closureId
end

local internalSolderInstanceRequire = nil

local sdkTable = { SDK = solder }
local solderInstanceIndex = SolderInstance.__index
local function makeVirtualRequire(script: SolderInstance): typeof(require)
    local function virtualRequire(target: any): any
        local targetType = type(target)
        local isSolderInstance = targetType == "table" and type(getmetatable(target)) == "table" and getmetatable(target).__index == solderInstanceIndex

        if (not isSolderInstance) and targetType ~= "string" and targetType ~= "number" then
            error("Attempted to call require with invalid argument(s).", 2)
        end

        if targetType == "number" then
            return (require)(target) --// Wrapping require to silence warning
        end

        if isSolderInstance and (target :: SolderInstance).ClassName ~= "ModuleScript" then
            error("Attempted to call require with invalid argument(s).", 2)
        elseif isSolderInstance then
            return internalSolderInstanceRequire(target)
        end

        if targetType == "string" and string.match(target, "^@%w+/solder$") then
            return sdkTable
        end

        local components = string.split(target, "/")
        local currentInstance: SolderInstance
        local pathPrefix = string.sub(components[1], 1, 1)
        if pathPrefix == "@" and components[1] ~= "@self" then
            error(`Unable to require module from given path '{target}'`, 2)
        end

        if components[1] ~= ".." and components[1] ~= "." and pathPrefix ~= "@" then
            error(`Unable to require module from given path '{target}'`, 2)
        end

        if components[1] == "@self" then
            currentInstance = script
        elseif components[1] == "." then
            if not script.Parent then
                error(`Unable to require module from given path '{target}'`, 2)
            end

            currentInstance = script.Parent :: SolderInstance
        else
            if (not script.Parent) or (not script.Parent.Parent) then
                error(`Unable to require module from given path '{target}'`, 2)
            end

            currentInstance = (script.Parent :: SolderInstance).Parent :: SolderInstance
        end

        for idx = 2, #components do
            local component = components[idx]
            if component == "." and (not script.Parent) then
                error(`Unable to require module from given path '{target}'`, 2)
            elseif component == "." then
                currentInstance = script.Parent :: SolderInstance
                continue
            end

            if component == ".." and ((not script.Parent) or (not script.Parent.Parent)) then
                error(`Unable to require module from given path '{target}'`, 2)
            elseif component == ".." then
                currentInstance = (script.Parent :: SolderInstance).Parent :: SolderInstance
                continue
            end

            local instance = currentInstance:FindFirstChild(component)
            if not instance then
                error(`Unable to require module from given path '{target}'`, 2)
            end

            currentInstance = instance
        end

        if currentInstance.ClassName ~= "ModuleScript" then
            error(`Unable to require module from given path '{target}'`, 2)
        end

        return internalSolderInstanceRequire(currentInstance)
    end

    return virtualRequire
end

internalSolderInstanceRequire = function(target: SolderInstance): {boolean | any?}
    if cachedRequires[target] then
        return cachedRequires[target]
    end

    local closure, closureId = getClosureFromSolderInstance(target)
    local output = table.pack(xpcall(closure(makeVirtualRequire(target), target :: (LuaSourceContainer & SolderInstance)), function(message)
        warn(buildErrorTree(closureId, message))
    end))

    if not output[1] then
        error("Requested module experienced an error while loading", 3)
    end

    table.remove(output, 1)
    errorIfInvalidRet(output)

    return table.unpack(output)
end

for _, entryPointIdx in entryPoints do
    local virtualScript = scripts[entryPointIdx]
    local closure = closures[entryPointIdx](makeVirtualRequire(virtualScript), virtualScript :: (SolderInstance & LuaSourceContainer))

    if solder.lineTrackingEnabled then
        task.spawn(xpcall, closure, function(message)
            warn(buildErrorTree(entryPointIdx, message))
        end)
    else
        task.spawn(closure)
    end
end]==]


--[[
    Bundler
]]

local process = require("@lune/process")
local roblox = require("@lune/roblox")
local luau = require("@lune/luau")
local fs = require("@lune/fs")

local bundledTemplate = require("./BundledTemplate")
local fileSeparator = process.os == "windows" and "\\" or "/"

local function sanitizeReplacement(replacement: string): string
    return (string.gsub(replacement, "%%", "%%%%"))
end

local function latin1Encode(character: string): string
    return "\\" .. table.concat({string.byte(character, 1, -1)}, "\\")
end

local Bundler = {}

function Bundler.IsBundleable(inputFilePath: string): (boolean, string?)
    local fileExtension = string.match(inputFilePath, `.?([^.{fileSeparator}/]*)$`)
    if fileExtension ~= "rbxm" then
        return false, "The input file's extension is not supported."
    end

    if not fs.isFile(inputFilePath) then
        return false, "The input file does not exist."
    end

    local readSuccess, readRet = pcall(fs.readFile, inputFilePath)
    if not readSuccess then
        return false, readRet
    end

    local function checkChildren(children: {Instance}): (boolean, string?)
        for _, child in children do
            local ok, reason = checkChildren(child:GetChildren())
            if not ok then
                return false, reason
            end

            if (not child:IsA("LuaSourceContainer")) and child.ClassName ~= "Folder" then
                return false, `The instance '{child:GetFullName()}' is of class '{child.ClassName}', which is not supported.`
            end
        end

        return true, nil
    end

    local serdeSuccess, serdeRet = pcall(roblox.deserializeModel, readRet)
    if not serdeSuccess then
        return false, (serdeRet :: any) :: string
    end

    return checkChildren(serdeRet :: {roblox.Instance & Instance}) --// To get Luau LSP's type definitions
end

function Bundler.Bundle(modelRoot: {roblox.Instance}, trackLines: boolean, rootOverridePath: string?, _environmentName: string?, logFn: (message: string) -> ()): string
    local environmentName = _environmentName or "SolderRuntime"

    if rootOverridePath then
        local paths = string.split(rootOverridePath, ".")

        for _, childName in paths do
            local oldModelRoot = modelRoot
            for _, childInstance in modelRoot do
                if childInstance.Name == childName then
                    modelRoot = childInstance:GetChildren()
                end
            end

            if modelRoot == oldModelRoot then
                error(`An instance named '{childName}' could not be found in the root override '{rootOverridePath}'.`)
            end
        end
    end

    local closuresMarkerLineNumber = table.find(string.split(string.gsub(bundledTemplate, "\r", ""), "\n"), "--MARKER:CLOSURES") :: number --// Stupid CRLF
    assert(type(closuresMarkerLineNumber) == "number", "must be a number")

    local closuresString = ""
    local lines: {number} = {}
    local chunkNames: {string} = {}
    local entryPoints: {number} = {}

    local currentClosureId = 0

    local function visitChildrenAndBuildStrings(children: {roblox.Instance}): string
        local scripts = ""

        for _, child in children do
            local childFullName = child:GetFullName()
            local childChildren = child:GetChildren()

            logFn(`Visiting '{childFullName}'...`)

            if not child:IsA("LuaSourceContainer") then
                local instanceCreationExpression = `SolderInstance.new("{latin1Encode(child.Name)}", "{child.ClassName}", {if child.Parent then "_parent" else "script.Parent"}, {#childChildren})`

                if #childChildren ~= 0 then
                    local childInstances = visitChildrenAndBuildStrings(childChildren)
                    scripts ..= `do\nlocal _parent = {instanceCreationExpression}\n{childInstances}end\n`
                else
                    scripts ..= `{instanceCreationExpression}\n`
                end

                continue
            end

            --// It's a LuaSourceContainer, so we increment the currentClosureId
            currentClosureId += 1

            if trackLines then
                local linesCount = #string.split(closuresString, "\n")
                table.insert(lines, closuresMarkerLineNumber + linesCount + 1)
                table.insert(chunkNames, `"{latin1Encode(childFullName)}"`)
            end

            if child.ClassName ~= "ModuleScript" then
                table.insert(entryPoints, currentClosureId)
            end

            local scriptSource = (child :: ((LocalScript | ModuleScript | Script) & roblox.Instance)).Source

            logFn(`Compile-checking '{childFullName}'...`)
            local compileSuccess, reason = pcall(luau.compile, scriptSource)
            if not compileSuccess then
                error(`Failed to compile: {reason}`)
            end

            closuresString ..= string.format([==[
function(require, script)
    return function(...)
        %s
    end
end,]==]
            ,(child :: ((LocalScript | ModuleScript | Script) & roblox.Instance)).Source)

            scripts ..= `scripts[{currentClosureId}] = SolderInstance.new("{latin1Encode(child.Name)}", "{child.ClassName}", _parent, {#childChildren})\n`

            if #childChildren ~= 0 then
                local preClosureId = currentClosureId
                local childInstances = visitChildrenAndBuildStrings(childChildren)
                scripts ..= `do\nlocal _parent = scripts[{preClosureId}]\n{childInstances}end\n`
            end
        end

        scripts ..= `_parent = nil\n`
        return scripts
    end

    local scriptsStringsLines = string.split(visitChildrenAndBuildStrings(modelRoot), "\n")
    local instancesStrings = ""
    local indent = 0
    for idx, line in scriptsStringsLines do
        if line == "end" then
            indent -= 1
        end

        instancesStrings ..= string.rep("\t", indent) .. line .. "\n"

        if line == "do" then
            indent += 1
            continue
        end
    end

    local moduleWithInstances = string.gsub(bundledTemplate, "--MARKER:INSTANCES", instancesStrings) --// No need to sanitize because they're already Latin1-encoded
    local moduleWithClosures = string.gsub(moduleWithInstances, "--MARKER:CLOSURES", sanitizeReplacement(closuresString))
    local moduleWithChunkNames = string.gsub(moduleWithClosures, "--MARKER:CHUNKNAMES", table.concat(chunkNames, ",")) --// Same here
    local moduleWithTrackedLines = string.gsub(moduleWithChunkNames, "--MARKER:TRACKEDLINES", table.concat(lines, ","))
    local moduleWithEntryPoints = string.gsub(moduleWithTrackedLines, "--MARKER:ENTRYPOINTS", table.concat(entryPoints, ","))
    local moduleWithEnvironmentName = string.gsub(moduleWithEntryPoints, "--MARKER:ENVNAME", sanitizeReplacement(environmentName))

    return moduleWithEnvironmentName
end

return {
    Bundler = Bundler,
    SDK = require("./SDK")
}


export type Solder = {
    --[[
        Invokes the real `require` function, instead of the virtualized one.
    ]]
    require: typeof(require),

    --[[
        The environment name that will be emitted in unhandled errors.

        Example: `<SolderRuntime> • (SomeFolder.Handler): An unexpected error occured.`, where `SolderRuntime` is the environment name.
    ]]
    environmentName: string,

    --[[
        The "real script", aka the `script` global in the current Luau environment.
    ]]
    script: LuaSourceContainer,

    --[[
        Reports whether line tracking was enabled for this build.
    ]]
    lineTrackingEnabled: boolean,

    --[[
        Gets the "real line", aka the line that would be in the original file.

        Example: A module calls `solder.getRealLine` and passes line 7100, and the module starts at line 7000 in the bundled file, then it would return 100.
    ]]
    getRealLine: (line: number) -> number,
}

type SolderClassNames = "Folder" | "Script" | "LocalScript" | "ModuleScript"
export type ISolderInstance = {
    ClassName: SolderClassNames,
    Parent: (Instance | SolderInstance)?,
    Name: string,

    _Children: {SolderInstance}
}

export type SolderInstanceImpl = {
    __index: SolderInstanceImpl,
    new: (name: string, className: SolderClassNames, parent: typeof(({} :: ISolderInstance).Parent), childrenSize: number?) -> SolderInstance,

    GetFullName: (self: SolderInstance) -> string,
    FindFirstChild: (self: SolderInstance, name: string, recursive: boolean?) -> SolderInstance?,
    WaitForChild: (self: SolderInstance, name: string, timeout: number?) -> SolderInstance?, --// Note that timeout is ignored.
    GetChildren: (self: SolderInstance) -> typeof(({} :: ISolderInstance)._Children)
}

export type SolderInstance = typeof(setmetatable({} :: ISolderInstance, {} :: SolderInstanceImpl))

local virtualSolder = {} :: Solder

virtualSolder.require = require
virtualSolder.environmentName = "SolderRuntime"
virtualSolder.script = script
virtualSolder.lineTrackingEnabled = true

virtualSolder.getRealLine = function(line)
    return line
end

return virtualSolder