Beanstalk projects are built around a small set of conventions:
#mod.bst = module API / visibility boundary
#page.bst = HTML builder entry/configuration convention
#config.bst = build/project configuration convention
At this stage of development, this is the only supported project structure to rely on. The rules are deliberately narrow so the compiler, dev server, and HTML builder can stay predictable while the language is still evolving.
The Beanstalk compiler frontend is shared by every project builder. It discovers modules, parses files, type checks code, lowers to HIR, and runs borrow validation. A project builder then decides how those compiled modules become real output files.
The current user-facing project builder is the HTML project builder. It consumes each compiled module and emits one HTML page per module entry file.
This split matters because Beanstalk is not designed as a single hard-coded web compiler. The same frontend can later feed other builders, such as a Wasm module builder, a documentation builder, or project-specific tooling.
| Layer | Responsibility |
|---|---|
| Compiler frontend | Discovers modules, resolves imports, checks types, lowers code, validates borrowing, and produces backend-ready module data. |
| Project builder | Interprets compiled modules for a specific project type and decides which output files to generate. |
| HTML project builder | Treats each module entry as a page and emits HTML files, with runtime code embedded as JavaScript by default. |
A normal HTML project has a #config.bst file at the project root. Config files use ordinary Beanstalk top-level constant syntax.
# project = "html"
# entry_root = "src"
# dev_folder = "dev"
# output_folder = "release"
# page_url_style = "trailing_slash"
# redirect_index_html = true
# name = "html_project"
# version = "0.1.0"
# author = ""
# license = "MIT"
# origin = "/beanstalk"
# html_lang = "en"
# html_title_postfix = " | Beanstalk"
# root_folders = {
@lib,
@assets,
}
Important settings:
| Setting | Meaning |
|---|---|
| #project | Selects the project type. For the documentation site and current web projects, use "html". |
| #entry_root | The source folder that is searched for module entry files. Usually "src". |
| #dev_folder | Where development builds are written. The dev server serves from here. |
| #output_folder | Where release builds are written when --release is used. |
| #root_folders | Extra top-level project folders that non-relative imports can resolve through before falling back to the entry root. |
| HTML-specific settings | Settings such as #origin, #html_lang, #html_title_postfix, #page_url_style, and #redirect_index_html are interpreted by the HTML builder. |
Config is loaded before module discovery. That means #entry_root and #root_folders shape how the project is searched and how imports resolve.
A module entry file is a Beanstalk file whose file name starts with #. In directory projects, the compiler searches the configured entry root for #*.bst files.
#config.bst is not a module entry.
Examples:
src/#page.bst -> module entry
src/docs/#page.bst -> module entry
src/docs/basics/#page.bst -> module entry
src/styles/docs.bst -> normal source file
#config.bst -> project config, not a module entry
Only module entry files have top-level runtime execution. The compiler treats the entry file's top-level runtime code as an implicit start function for that module.
Normal source files can contain imports, functions, structs, choices, type aliases, and constants. They should not rely on top-level runtime statements being executed by themselves.
The compiler discovers #*.bst files as module entries. The project builder decides what those entries mean.
The HTML project builder currently treats them as page entries. The file name and folder decide the output route.
| Entry file | HTML output path |
|---|---|
| src/#page.bst | index.html |
| src/docs/#page.bst | docs/index.html |
| src/docs/basics/#page.bst | docs/basics/index.html |
| src/#about.bst | about/index.html |
This gives pages stable folder-backed routes. A #page.bst file means "the index page for this folder". Other #name.bst files become named routes under the same folder.
Top-level templates are special. In the HTML project builder, direct top-level templates in a module entry file become page fragments.
There are two forms:
| Form | Meaning |
|---|---|
| #[...] | A top-level const template. It must fold at compile time and is emitted as static HTML. |
| [...] | A top-level runtime template. It is evaluated by the module's implicit start function and inserted into the page at runtime. |
Example:
import @styles/docs {navbar, section, theme_head}
# page_title = "Home"
# page_description = "Welcome page."
# page_head = theme_head
-- Const fragments are folded by the compiler and emitted directly.
#[navbar]
#[section, $markdown:
# Welcome
This part is static.
]
name = "Beanstalk"
-- Runtime fragments are produced by the implicit start function.
[section, $markdown:
Runtime hello from [name].
]
The HTML builder preserves source order between static and runtime fragments. Static fragments are emitted immediately. Runtime fragments use generated slots in the HTML document, then the embedded JavaScript calls the module start function and fills those slots.
Do not put page-producing top-level templates in helper files. Helper files should export reusable declarations, then page entries should import and use them.
The # prefix on top-level declarations controls module visibility.
| Declaration kind | Meaning of # |
|---|---|
| Variable declaration | Creates an exported compile-time constant. |
| Function | Exports the function so other files in the module can import it. |
| Struct or choice | Exports the type. |
| Type alias | Exports the alias. |
| Top-level template | #[...] is not a normal exported value. It is an entry-file-only const page fragment. |
Regular top-level declarations without # are private to their file/module context and cannot be imported from other files.
Example helper file:
-- src/components/card.bst
# CardData = |
title String,
body String,
|
# render_card |card CardData| -> String:
return [:
<article>
<h2>[card.title]</h2>
<p>[card.body]</p>
</article>
]
;
private_note || -> String:
return "This is not exported."
;
Example page entry:
-- src/cards/#page.bst
import @components/card {CardData, render_card}
# page_title = "Cards"
card = CardData("Hello", "This was rendered by a helper function.")
[:
[render_card(card)]
]
Imports target exported symbols, not whole files.
-- Single exported symbol
import @components/card/render_card
-- Grouped exported symbols
import @components/card {CardData, render_card}
-- Local alias
import @components/card/render_card as card
-- Relative import from the current file's folder
import @./shared/title/render_title
-- Parent-folder relative import
import @../components/card {CardData, render_card}
Path resolution is project-aware:
Builder-provided and project-local source libraries expose their prefix directly. That means import @html ... resolves through the HTML builder's @html library, while import @styles/docs ... resolves through docs/src/styles/docs.bst.
The Beanstalk documentation website is itself a Beanstalk HTML project. Ignoring markdown notes and generated files, its source shape is roughly:
docs/
├── #config.bst
├── src/
│ ├── #page.bst
│ ├── docs/
│ │ ├── #page.bst
│ │ ├── basics/#page.bst
│ │ ├── templates/#page.bst
│ │ ├── functions/#page.bst
│ │ ├── if-statements/#page.bst
│ │ ├── loops/#page.bst
│ │ ├── collections/#page.bst
│ │ ├── structs/#page.bst
│ │ ├── choices/#page.bst
│ │ ├── pattern-matching/#page.bst
│ │ ├── aliases/#page.bst
│ │ └── progress/#page.bst
│ └── styles/
│ └── docs.bst
├── lib/
│ └── local_helpers/
│ └── #mod.bst
├── @assets/
│ └── ...
├── dev/
└── release/
The important part is the split:
The build command compiles the project, then writes the builder's output files under the selected output root.
| Build mode | Output root |
|---|---|
| Default / dev | #dev_folder, usually dev. |
| --release | #output_folder, usually release. |
In the default JS-backed HTML mode, each page module produces an HTML file. The generated JavaScript is embedded into that HTML file.
In the experimental --html-wasm mode, the builder can instead emit a route folder containing:
The HTML builder owns stale-output cleanup for generated HTML artifacts. Generated output folders should be treated as build artifacts, not source.
Source libraries are normal .bst source roots with an explicit facade file. Project-local libraries live under lib/
#mod.bst is the visibility gate for a source library module.
It defines what other modules can import from that library.
lib/ui/
├── #mod.bst
├── button.bst
└── layout.bst
-- lib/ui/#mod.bst
#import @./button/button
#import @./layout {page as ui_page}
Rules:
Every builder provides the core prelude surface. Optional core libraries require explicit imports and builder support:
The HTML builder also provides @html. Builder libraries are not core libraries; other builders may expose different library roots.
Deferred library features include package versions, remote fetching, lockfiles, source-library HIR caching, namespace imports, wildcard imports, seeded random, full date/time APIs, and automatic docs extraction from #mod.bst.
A future #test.bst file can hold project tests. Normal builds would ignore it. A planned test command could discover and run those files, similar to how cargo test discovers Rust tests.
This is separate from the current compiler-internal integration test runner.
The $doc style directive can eventually make documentation part of Beanstalk source. Top-level or nested doc templates could be stripped from normal output code, then collected by a documentation command.
This would make documentation generation feel closer to Rust doc comments, but with Beanstalk's template system instead of comment syntax.