Documentation / Packages and imports

Packages and imports

Imports make names visible. Module roots decide what crosses a module boundary. Project-local packages give reusable modules stable prefixes.

Those jobs stay separate. Keeping them separate stops a filesystem from quietly becoming an API.

Choose the explanation level: Import paths

Import paths

Imports make names visible. Every source import resolves from the importing file's owning module root, not the file's directory.

A module-root-relative import


    import @card {render_card}
    

@card resolves to card.moth under the importing file's owning module root, regardless of how deep the importing file is inside the module.

A support package import


    import @markdown {render}
    

@markdown resolves to a scoped +*.moth support package visible in the importing module's lexical scope.

Import paths resolve declaration surfaces, not file bytes. Every source import resolves from the importing file's owning module root, not the file's physical directory.

Path families

  • Module-root-relative: imports resolve from the importing file's owning module root. Example: inside src/internal/deep/renderer.moth, import @accounts { Account } resolves to src/accounts.moth, not src/internal/deep/accounts.moth.
  • Child module: reaching a child normal module ends filesystem traversal and exposes only its export: facade. Imports cannot bypass that facade with paths such as @child/internal.
  • Support package: scoped +*.moth support packages are imported by package name. Consumers do not encode ancestor paths.
  • Core binding-backed package: @core/math, @core/text and current peers are registered by the builder.
  • Builder source-backed package: @html is a builder-provided source-backed package.
  • Builder binding-backed package: @web/canvas is a builder-owned runtime package.
  • JavaScript file: project-local annotated JavaScript files require .js and a builder external import provider. The .js extension is kept.

Resolution rules

  • @./... has no supported meaning.
  • Parent components and .. are invalid.
  • Paths may traverse ordinary unrooted directories owned by the same module.
  • .moth, .mtf and .md source imports are extensionless.
  • Direct .js imports keep the .js extension.
  • Scoped support packages are injected by package name, not physical ancestor paths.
  • Core and builder runtime packages use their registered package path.
  • Specific symbol imports use grouped syntax. Direct symbol-path imports such as import @core/math/sin are rejected.
  • Direct imports of +*.moth support root files and config.moth are rejected.
  • @@name is invalid. The leading @ starts an import path and is not part of the module name. Normal module-root filenames such as @page.moth are not imported directly.
  • Moth template .mtf and plain Markdown .md content files both expose generated content constants. See Moth template content imports and Markdown imports.
  • The imported public surface of a module root comes from its export: block. See Module visibility.

Same-module imports

Files in the same module can import ordinary sibling source files directly. Same-module imports bypass the public-surface boundary. See Module visibility.

Cross-module imports

Imports that cross into another module boundary can only access what that module's export: block makes public. Cross-module imports cannot bypass the public API. See Module visibility.

Content-file imports

Moth template and plain Markdown content files are imported with the same extensionless syntax. Their import shape is covered by the Moth template and Plain Markdown pages, not here.

Related concepts

Choose the explanation level: Grouped and namespace imports

Grouped and namespace imports

Grouped imports bring multiple symbols. Namespace imports bring a whole surface.

Two colliding render symbols


    import @blog { render as render_blog }
    import @docs { render as render_docs }
    

Two modules both export render. Aliases keep them apart.

A namespace alias


    import @core/math as math

    result = math.sin(1.0)
    

math gives you field access to the whole math package.

Grouped and namespace imports provide compact syntax for importing multiple symbols or a whole declaration surface.

Grouped symbol imports


    import @core/math {sin, cos, PI}

    import @components {
        render as render_component,
        Button as UiButton,
        Card,
    }
    

Each entry in a grouped import brings one symbol into the file. as creates a file-local alias. Grouped imports expand into individual symbol imports.

Namespace imports


    import @core/math

    angle = math.PI
    result = math.sin(angle)
    

A namespace import brings the whole surface as a field-access-only record. Use math.sin(1.0) for values and math.PI for constants, but do not pass the namespace record itself as a value.

Namespace alias


    import @core/math as math

    import @vendor/drawing.js as drawing
    

A namespace alias gives the namespace a local name. This is the standard form for external package namespaces.

Grouped symbol alias


    import @core/math {
        sin as sine,
        PI as pi,
    }
    

Each entry can have its own alias. There is no trailing group-level alias.

Importable symbol categories

  • functions
  • structs
  • choices
  • type aliases
  • traits
  • compile-time constants

Runtime top-level bindings are not importable. See Module visibility.

Namespaced access

Source and module namespace imports are shallow. External package namespaces can expose nested package-local symbols. Use field-access syntax on the namespace record, but do not pass the namespace itself as a first-class value. Source-authored receiver methods are called through receiver-call syntax rather than as namespace fields.

Imports are compile-time declarations

Import records are not first-class runtime values. They exist at compile time to make names visible.

Collision and no-shadowing policy

  • Ordinary import aliases are file-local.
  • An import alias cannot hide an existing visible name.
  • If render is already declared in the file, import @other { render } is an error. Pick a different local name.
  • Leading-case mismatches warn. For example, importing a struct with a lowercase alias or a function with an uppercase alias triggers a warning.

No trailing alias for a group

A grouped import cannot have a trailing group-level alias. Each entry gets its own alias individually.

Related concepts

Choose the explanation level: Module visibility

Module visibility

Declarations are private to their module. The export block makes them public.

An internal helper file


    -- src/components/utils.moth
    internal_value #= 42
    

The child root uses it directly


    -- src/components/@api.moth
    import @utils {internal_value}

    export:
        import @utils {internal_value}
    ;
    

Same-module imports see everything. No export block needed.

Other modules need the export block


    -- src/@site.moth
    import @components {internal_value}
    

Now the parent module can import internal_value through the @components public surface. It cannot bypass that surface with @components/utils.

Module visibility controls which declarations cross a module boundary. Normal declarations are private to their module. The export: block marks what other modules can see.

Same-module internal visibility

Files within the same module can import each other's declarations directly. A module is defined by its root directory. All .moth files in that directory (and its subdirectories that do not have their own root) belong to the same module.

Given this topology:


        src/
        ├── @site.moth
        └── components/
            ├── @api.moth
            └── utils.moth
    

utils.moth and @api.moth belong to the same components child module. Same-module imports can see private declarations.

Internal helper:


    -- src/components/utils.moth
    internal_value #= 42

    internal_fn || -> String:
        return "internal"
    ;
    

Child root:


    -- src/components/@api.moth
    import @utils {
        internal_value,
        internal_fn,
    }

    export:
        import @utils {
            internal_value,
        }
    ;
    

Same-module imports bypass the public-surface boundary. The importing file sees all declarations in the target file.

Cross-module public boundary

A different module can only access declarations marked public by the module root's export: block. Cross-module imports cannot bypass the public API.


    -- src/@site.moth
    import @components {
        internal_value,
    }
    

The parent module sees only the components child module's export: surface. It cannot bypass that boundary with import @components/utils {internal_value}. The @components/utils path cannot bypass the child facade.

Without the root export block, the public import is rejected with MOTH-IMPORT-0011.

Normal modules are not source packages

An @*.moth normal module is not a source package. Use +package.moth when the example intends to teach a scoped package. See Project-local packages for the support-package contract.

Root-private declarations

Declarations in the module root file outside the export: block are private to the root. They are not available to other modules even through the public surface.

Direct root import rejection

Direct imports of normal module-root files are rejected. Import the module through its directory path, not through the root filename.

Runtime top-level bindings not importable

Runtime top-level bindings are start-body code. They are not importable declarations. Only compile-time constants, functions, structs, choices, type aliases and traits are importable.

Receiver method visibility

Receiver methods are visible through a module's public surface when the module exports the receiver type's source surface. A type alias in the export block does not automatically re-export private implementation methods. Export the receiver type's source surface or a wrapper explicitly.

Import cycles

Circular imports between files in the same module are accepted when the declarations resolve in dependency order. Circular constant dependencies are rejected with MOTH-RULE-0033.

No public access through filesystem tricks

There is no way to bypass the module public-surface boundary by choosing a different import path. All cross-module imports go through the same resolution and visibility check.

Related concepts

Choose the explanation level: Public re-exports

Public re-exports

The export block can re-export imported symbols so other modules can use them.

A public re-export


    export:
        import @components/card {
            CardData as Card,
            render_card,
        }
    ;
    

Other modules can now import Card and render_card through the module's public surface. CardData is not also exported unless the block lists it separately.

Local alias is not public


    import @components/card {CardData as LocalCard}
    

LocalCard is file-local. It is not public. Only imports inside the export: block become public re-exports.

The export: block can re-export grouped imports from other files, creating a public API surface that exposes imported declarations under controlled names.

Grouped re-export


    export:
        import @components/card {
            CardData as Card,
            render_card,
        }

        import @core/math {
            PI,
            sin,
        }
    ;
    

Card, render_card, PI and sin become accessible to cross-module imports. A grouped alias inside export: becomes the public name. The original CardData name is not also exported unless it is listed separately.

Renamed public re-export

The as alias inside an export-block re-export defines the public API name. The original source name is what the module imports privately. The alias is what other modules see when importing the module's public surface.

Exported imported types, constants and functions

Any importable declaration can be re-exported: functions, structs, choices, type aliases, traits and compile-time constants.

Re-export does not make local import alias automatically public

A private import @path { Symbol as LocalName } outside the export block creates a file-local alias. It does not become public. Only an import inside the export: block creates a public re-export.

Public name collisions

Public names in the export block must not collide with each other. Duplicate public names are rejected.

Invalid unknown or private targets

Re-exporting a symbol that does not exist in the target file is rejected. Re-exporting a private declaration that is not visible in the target file is rejected.

No legacy inline export syntax

Legacy inline export syntax (without the : block delimiter) is rejected with MOTH-SYNTAX-0001. The strict export: block is the only public API marker. See Public API for the full export-block rules.

Related concepts

Choose the explanation level: Project-local packages

Project-local packages

Scoped support packages are +*.moth roots in the project source tree. The containing directory supplies the package name.

A simple support package


        src/
        ├── @site.moth
        └── blog/
            ├── +package.moth
            └── helpers/dates.moth
    

+package.moth makes @blog a scoped support package visible to @site and its sibling modules.

Importing from a package


    import @blog {post_title}
    

@blog resolves to the blog directory's support root. The package's public surface comes from its export: block.

Project-local source-backed packages are scoped +*.moth support packages discovered structurally from the project source tree. The current compiler also retains legacy config-driven package_folders discovery, defaulting to lib/. The queued Project Config migration removes that compatibility path.

Directory structure


        project-root/
        ├── config.moth
        ├── src/
        │   ├── @site.moth
        │   ├── markdown/
        │   │   ├── +package.moth
        │   │   ├── parser/
        │   │   │   └── @parser.moth
        │   │   └── rendering/
        │   │       └── @rendering.moth
        │   └── pages/
        │       └── @pages.moth
        └── +package.moth
    

The markdown directory contains a +package.moth root, making @markdown a scoped support package visible to @site, @pages and their descendants.

Scoped visibility

For a support package S whose nearest ancestor normal module is P:

  • S is visible to P
  • S is visible to normal sibling modules of S and their descendants
  • S is not visible above P
  • S is not visible outside P's subtree
  • S is not visible inside its own private implementation subtree
  • another support package in the same owner scope cannot import S

Public surface

Outside importers can only see declarations and grouped re-exports marked public by the support root's export: block. Internal implementation files are hidden unless the root re-exports them.

Private subtrees

The support facade may import ordinary files it owns, any descendant module in its private subtree, support packages from a strictly outer scope and registered packages. It may not import its parent, normal sibling consumers or same-scope support siblings.

Consumers see only the support facade's export: surface. They cannot address @markdown/parser or another implementation path.

Imported package roots do not produce routes

A support package root is imported rather than activated as an HTML route. Support roots reject top-level runtime work and page fragments. They export declarations but produce no HTML artifact. See HTML routing and artifacts.

Origin and backing are separate

Scoped +*.moth packages are ProjectLocal packages backed by Moth source. @html is also source-backed but has Builder origin. @core/math and @web/canvas are binding-backed packages with Core and Builder origins respectively. See Package origins and backing.

Related concepts

Choose the explanation level: Package origins and backing

Package origins and backing

Packages can differ in two ways: who provides them and what backs their implementation.

Origin

  • Core packages come from the compiler.
  • Builder packages come from the selected project builder.
  • ProjectLocal packages come from your project.

Backing

  • Source-backed packages contain ordinary Moth modules.
  • Binding-backed packages expose typed functions, constants and opaque types implemented outside Moth.

@html is a source-backed Builder package. @core/collections and @core/math are binding-backed Core packages. @web/canvas is a binding-backed Builder package.

Every package has two independent classifications: where it comes from and how the compiler obtains its implementation.

Package origin

Origin describes ownership and distribution:

  • Core: compiler-owned packages such as @core/io and @core/math.
  • Builder: packages selected by a project builder, including @html and @web/canvas.
  • ProjectLocal: scoped +*.moth support packages, the project-root package facade and annotated project-local JavaScript imports.
  • Standard and Dependency: reserved for future package-system support.

Package backing

Backing describes implementation:

  • MothSource: one or more Moth modules resolved through the source package registry.
  • ExternalBinding: typed functions, constants and opaque types implemented outside Moth.

Package

Origin

Backing

@html

Builder

MothSource

@core/collections, @core/io, @core/math, @core/text, @core/random, @core/time

Core

ExternalBinding

@web/canvas

Builder

ExternalBinding

Scoped +*.moth support package

ProjectLocal

MothSource

Project-root +*.moth facade

ProjectLocal

MothSource

Annotated project-local .js import

ProjectLocal

ExternalBinding

Origin and backing don't change import syntax or module visibility. Source-backed packages contain ordinary modules with strict export: public surfaces. Binding-backed packages expose typed external symbols registered by the builder.

Prelude is visibility policy

The prelude is implicit-import policy, not an origin or backing. It exposes the bare io namespace as an alias to @core/io. Package metadata belongs to @core/io, while bare-name visibility belongs to the prelude.

Builder-selected availability

The builder decides which packages exist. Unsupported packages are rejected with an unsupported-by-builder diagnostic. The HTML builder opts into @html, @web/canvas and current optional Core packages.

Binding-backed package constraints

Binding-backed packages expose free functions, constants and opaque types. They don't expose source-style receiver methods. Use a source-backed wrapper type for method-style APIs. External calls are positional-only because named arguments, default parameters and reactive parameters are source-function features.

The HTML-Wasm mode has a stricter backend surface. A reachable JS-backed package needs a Wasm lowering before that backend can use it.

No package manager yet

There is no remote fetching, lockfile or version resolution yet. Use +*.moth support packages for project-local source-backed packages, builder-provided package imports for built-in capabilities and annotated .js wrappers for direct browser integration.

Related concepts

Choose the explanation level: External binding contracts

External binding contracts

External binding packages give you typed functions and opaque types from code written outside Moth. The HTML builder supports annotated JavaScript imports and will support WIT component imports in the future.

Annotated JavaScript

You can import .js files that use @moth.opaque and @moth.sig annotations. The annotations tell the compiler what Moth names and types to expose.


    import @vendor/drawing.js as drawing
    

JavaScript export names are runtime details. Moth names come from annotations, not from the JavaScript export.

What JavaScript can export

The builder accepts export function name(...) { ... } and block-bodied arrow exports. @moth.sig annotations expose free functions.

What JavaScript can't export

The builder rejects default exports, re-exports, CommonJS, classes, callbacks, async functions, property accessors and multi-success returns. These don't map to Moth's type system.

Values cross by value

Moth values that cross into JavaScript go by value. JavaScript can't hold references into Moth storage. Opaque handles represent foreign identities, not Moth references.

WIT is deferred

Future Wasm component imports will use a value-only WIT profile. WIT resources, callbacks, async and streams are deferred. See External binding contracts advanced for the full deferred surface. For the full binding contract, rejected JS features and deferred WIT surfaces, see External binding contracts advanced.

External binding packages expose typed functions, constants and opaque types implemented outside Moth. The HTML builder supports two binding profiles: annotated project-local JavaScript imports and future value-only WIT component imports.

Annotated JavaScript imports

The HTML builder supports annotated single-file .js imports through @moth.opaque and @moth.sig. @moth.sig declares each exposed function. @moth.opaque is required only when signatures use foreign opaque handle types. A function-only module may use @moth.sig without @moth.opaque. JavaScript export names are runtime implementation details. Moth names come from annotations.


    import @vendor/drawing.js as drawing
    

Annotated JavaScript example


    /**
     * @moth.sig emphasize |text String| -> String
     */
    export const emphasize = (text) => {
        return `**${text}**`;
    };


    /**
     * @moth.sig join_label |prefix String, body String| -> String
     */
    export function joinLabel(prefix, body) {
        return `${prefix}: ${body}`;
    }
    

The Moth import uses the annotation names, not the JavaScript export names:


    import @vendor/drawing.js { emphasize, join_label }
    

Supported JavaScript export forms

  • export function name(...) { ... }
  • block-bodied arrow exports

@moth.sig annotations expose free functions. this receiver-style signatures are rejected during registration. Runtime imports from builder-registered modules must be named static imports.

Exact signature subset

  • parameters are positional and typed
  • mutable parameters use name ~Type
  • signatures have zero or one success return
  • an optional final Error! channel is allowed
  • Error! must be final
  • generic signatures are rejected
  • option and collection types are rejected
  • callbacks are rejected
  • multi-success returns are rejected
  • this receiver-shaped signatures are rejected for external package registration
  • opaque types must be declared through @moth.opaque before use where required
  • JavaScript export names are runtime implementation details
  • Moth names come from annotations

Rejected JavaScript features

  • arbitrary dependency graphs
  • default exports
  • re-exports
  • CommonJS
  • classes
  • JS constants
  • property accessors
  • callbacks
  • async functions
  • collections or options in JS signatures
  • generic external type
  • receiver methods
  • multi-success JS returns

Restricted host-binding profile

Existing Core, Builder and JavaScript-backed bindings follow a restricted host-binding profile:

  • ordinary Moth values cross by value
  • host code may not retain references into ordinary Moth storage
  • opaque handles represent foreign identities rather than Moth reference types

Observable external resources require explicit close or teardown. Host finalisation timing does not define language behaviour.

WIT value-only component profile

WIT component imports are accepted but deferred. Future general Wasm library imports use a value-only WIT component profile:

  • supported arguments lower from shared reads into independent component values
  • results lift into independent Moth result graphs
  • no Moth alias, lifetime owner, ownership state or destruction responsibility crosses the component boundary

Deferred WIT surfaces

The V1 value-only profile defers WIT resources, callbacks, async operations, futures, streams, shared-memory views, raw pointers, returned aliases and retained Moth references.

Related concepts

Package reference pages