lloyAlloy on GitHub
UnknownModule
Docs

UnknownModule

Updated

The rule behind this error is on the Modules page.

import { a, b as c, type T } from "./m"
import * as M from "./m"
import Name from "./m"
import M, { a, type T } from "./m"
import type { T } from "./m"

Brings names from another module into this file. { } picks exports by name, as renames one, and * as M takes the whole module, where the default sits under M.default.

A bare name takes the module's export default, whatever the name here is: import Name from "./m" is local Name = require("./m").default. A module with no export default reports on that line and names the export you probably meant. The default and the named exports are separate, so import M, { a } takes both in one line. A plain Luau module and a data file have no export table: a bare name takes the value they return.

An Alloy module that ends in return <expr> and exports nothing reads the same way. The returned value is the module: a bare name binds it, * as M binds it, and a name in braces reads one key of it when the compiler can see the keys, which it can for a table literal, for a local the file fills in by name, and for a struct the file constructs. A key the value does not carry reports. A module that returns a value and exports names too reports on the path; use one or the other.

type marks a type-only import, which costs nothing at runtime. Two modules may import types from each other this way. luau-lsp types no cycle of requires, so in alloy flux the names of the import that closes the cycle read as any. An attribute imports by its bare name, import { tagged }, and @tagged reads it; through import * as M it reads @M.tagged.

An import resolves where it stands: the require runs on its own line, as Luau reads the file. One inside a function or a do block binds its names for that scope alone, so a name used in one place can come in there.

import("./m") is the expression form. It gives the value of the module, as a bare name reads it: the export default of a module that has one, and the whole value of any other. A table with a default key reads as an export table. The value takes its type from the module when the path is a string or an instance chain. import<<T>>(expr) gives the value of a dynamic path the type T; without it the value is unknown. import "./m" is the same call, so on a line of its own it runs a module for its effects.

A path that ends in .json or .toml imports a data file as a table; alloy doc data explains.

A namespace comes in as one name: import { Math } from "./math" binds the group and every type it carries, so Math.Vec2 reads in a type slot here too.

A Roblox service is an import too, under the @game alias. import Players from "@game/Players" names one service, and import { Players, ReplicatedStorage } from "@game" names any number, with as to rename one. Both lower to local Players = game:GetService("Players") on the import's own line, so the binding carries the service class. "@game" takes the braces and "@game/X" takes a bare name; the other way round reports the form to write, and a name that is no service names the nearest one.

@game is one namespace with two readings, and what follows the service decides which. One segment after the alias, and it names a service, is the service itself. A longer path, "@game/ReplicatedStorage/Shared/economy", is an instance path, which is what the ship artifact writes for an @alias require, and it resolves the way a module path does.

@game is reserved: a [mount] table or a .luaurc that declares an alias of that name is an error, since the compiler answers the name itself. alloy doc mount has the rest.

"game" and "game:Players" were the spellings before the alias. They still parse and lower the same way, and the game_alias lint asks for the alias form; alloy flux --fix rewrites the path. This release is the last that reads them.

The word is also free as a name. import before *, {, type {, or Name from opens the statement; local import = {}, import.cache, and t.import read the Luau way. One shape stays the keyword: a call spelled import(...), import "...", or import<<T>>(...) is always the module import, because the emit turns it into a require. A file that binds a local named import reaches it by field or by another name.

An import into another project builds that project by its own alloy.toml and requires its output; the project file mounts that output like any folder.