External Modules#

Bialet supports importing external Wren modules from remote sources, so you can use community-created libraries without manually downloading and managing them. This page covers using them, authoring your own, and managing the cache.

Import Syntax#

2. Full URL#

import "https://example.com/path/to/module.wren" for ClassName
  • You must include the .wren extension in the URL.

  • The URL must return raw Wren source code, not HTML.

  • For GitHub, use raw.githubusercontent.com URLs.

import "https://raw.githubusercontent.com/4lb0/emoji/main/emoji.wren" for Emoji

How It Works#

  1. Cache check — Bialet checks whether the module is already cached in the BIALET_REMOTE_MODULES database table.

  2. Download — if not cached, it downloads the module via HTTP GET.

  3. Validation — checks for a 2xx status code.

  4. Store — the content is stored in the database, keyed by import path.

  5. Load — the module is loaded and made available to your code.

The first load needs an internet connection; every load after that reads from the local database — fast, and works offline. Cached modules persist until you explicitly clear them.

⚠️ Pitfall: cached modules don’t auto-update. If you push a fix to the main branch of an imported module, running instances keep using the old cached copy until you clear the cache. This is deliberate — treat it as a stability feature, not a bug.

When to Use an External Module#

Bialet’s philosophy here is the opposite of the Node ecosystem. Node culture normalizes pulling in a package for a one-line function (is-odd, left-pad) because npm install is nearly free and the dependency graph is invisible day-to-day. In Bialet, every import "gh:..." is a real, visible network fetch the first time it runs, a row cached forever in your app’s SQLite file, and code that runs with the full privileges of your app.

A one-line helper doesn’t clear that bar. If a function fits in a few lines and has no dependencies of its own, write it directly in your domain class or a local _app/*.wren file — it costs nothing to maintain, has no network dependency, and can’t change or disappear out from under you.

Reach for an external import when the library does something genuinely substantial: nontrivial parsing, a sizable data set, an algorithm you don’t want to re-implement and verify yourself. Reserve it for real dependencies, not for saving five lines of typing.

Authoring an External Module#

There is no package format, manifest, or build step. An external module is just a plain .wren file, reachable over HTTP, that defines one or more classes (or top-level vars) with a construct/static API — exactly like any local _app/*.wren file you’d import in your own project.

Minimal Example#

A single-file library published on GitHub, e.g. 4lb0/emoji/emoji.wren:

// emoji.wren — the entire module
class Emoji {
  static shrug { \\_(ツ)_/¯" }
  static wave(name) { "👋 %(name)" }
}

Consumers import it with:

import "gh:4lb0/emoji/emoji" for Emoji
System.print(Emoji.wave("world"))

That’s the whole contract: push a .wren file to a public repo (or serve it from any URL that returns raw Wren source), and it’s importable.

Conventions for Publishing#

  • One purpose per file. Keep the module focused; consumers only pay the cache/download cost for files they actually import.

  • PascalCase class names, matching the rest of the Wren style used throughout Bialet apps.

  • Avoid side effects at the top level. Code outside a class body runs the moment the module loads. Stick to class and method definitions — don’t run queries, print logs, or mutate state just by being imported.

  • Tag releases with git tags (v1.0, v1.1, …) instead of only publishing to main. Consumers pin @v1.0 for stability; you keep main for in-progress work. This is for your consumers’ benefit — :ref:Cache Management for why pinning matters on the consuming side.

  • Document the exported API with a README.md in the repo (a Wren doc-comment format doesn’t exist) — show the exact import line, since that’s the one thing every consumer needs verbatim.

  • Test it like any Bialet app before publishing: point bialet -t at the file with a throwaway app root and exercise the exported classes. See Tests.

Multi-File Modules and Self-Imports#

A relative import inside a remote module (e.g. import "helper" inside gh:someuser/mylib/main.wren) resolves relative to that module’s own owner/repo/path — not your app’s local files — so it correctly fetches gh:someuser/mylib/helper from the same repo instead of looking for a local helper.wren in your app.

The one thing this doesn’t preserve is a version tag. A relative import always resolves to the sibling file’s default branch, even when the importing file itself was loaded with @tag:

// File: gh:someuser/mylib/main.wren, imported as "gh:someuser/mylib/main@v1.0"
import "helper" for Helper
// ❌ This actually fetches gh:someuser/mylib/helper (main branch), not
// gh:someuser/mylib/helper@v1.0 -- the @v1.0 from main's own import path
// is not carried over to its relative imports.

For anything you publish with tagged releases, always reference your own repo explicitly with the matching tag instead of a plain relative import:

// ✅ Correct — the tag travels with the reference, the same way any
// consumer would pin it.
// File: gh:someuser/mylib/main.wren, tagged v1.0
import "gh:someuser/mylib/helper@v1.0" for Helper

⚠️ Pitfall: if you tag a new release and forget to bump the internal @tag references inside the module itself, older files can end up importing a newer (or newer files an older) internal helper than intended. Bump every internal self-reference together with the release tag.

Cache Management {#cache-management}#

External modules are cached in _db.sqlite3, table BIALET_REMOTE_MODULES:

CREATE TABLE IF NOT EXISTS BIALET_REMOTE_MODULES (
  module TEXT PRIMARY KEY,      -- The import path (e.g., "gh:user/repo/path")
  content TEXT,                 -- The cached Wren source code
  createdAt DATETIME DEFAULT CURRENT_TIMESTAMP
)
-- Clear all cached external modules (forces re-download on next import)
DELETE FROM BIALET_REMOTE_MODULES;

-- View all cached modules
SELECT module, createdAt FROM BIALET_REMOTE_MODULES;

-- Check cache size
SELECT COUNT(*), SUM(LENGTH(content)) as total_bytes
FROM BIALET_REMOTE_MODULES;

After clearing the cache, restart your Bialet application (or trigger a reload) to re-download the modules.

Refreshing a Module Programmatically#

Since the cache is just a table, you can clear a specific row from Wren code with a normal Query object — no shell access needed:

`DELETE FROM BIALET_REMOTE_MODULES WHERE module = ?`.query("gh:yourorg/internal-lib/utils@main")

The next time any request imports that module, the cache miss triggers a fresh HTTP download. This is most useful for internal libraries you control, shared across several Bialet apps, where you want changes to propagate without touching every consumer’s database by hand. Trigger it from a periodic cron job:

// _cron.wren
// Pull the latest internal shared library once a day at 3 AM.
Cron.at(3, 0) { |d|
  `DELETE FROM BIALET_REMOTE_MODULES WHERE module = ?`
    .query("gh:yourorg/internal-lib/utils@main")
}

Or from an admin-only route, so a person can force a refresh on demand:

// _app/admin/refresh-module.wren
if (Request.isPost) {
  var mod = Request.post("module") || ""
  if (mod != "") {
    `DELETE FROM BIALET_REMOTE_MODULES WHERE module = ?`.query(mod)
  }
  return Response.redirect("/admin/refresh-module?done=1")
}

⚠️ Pitfall: this deliberately undoes the stability/security guarantee described above — a module can now change between requests without a deploy. Only wire this up for modules you own or fully trust, and never for third-party libraries pinned to main for convenience.

GitHub URL Format#

gh:owner/repo/path@branch is internally converted to:

https://raw.githubusercontent.com/owner/repo/refs/heads/branch/path.wren

Import Statement

Generated URL

gh:user/lib/utils@dev

https://raw.githubusercontent.com/user/lib/refs/heads/dev/utils.wren

gh:org/pkg/sub/module@v1.0

https://raw.githubusercontent.com/org/pkg/refs/heads/v1.0/sub/module.wren

  • The .wren extension is added automatically.

  • Default branch is main if unspecified.

  • The path must resolve to a valid Wren file in the repository.

  • Invalid paths or missing files trigger error messages in the logs.

Error Handling#

“Invalid GitHub URL” — the import path doesn’t follow gh:owner/repo/path; owner, repo, or file path is missing.

“Module not found in GitHub” — file doesn’t exist at that path, the branch/tag doesn’t exist, the HTTP request returned a non-2xx status, or there’s a network problem.

“Import type not supported” — the import uses a protocol other than gh:, http://, or https://.

Check the Bialet logs for details:

System.print("Debug: attempting import...")

Security Considerations#

⚠️ Pitfall: external imports download and execute code from remote sources, with the same privileges as your application. Treat an import statement like adding a dependency, not like a link.

  • Verify the source before importing.

  • Review the code on GitHub when possible.

  • Use version tags, not main, for stability and predictability.

  • Cache behavior is a security feature — once downloaded, a module won’t silently change under you.

// ✅ Good: well-known, maintained library, pinned version
import "gh:4lb0/emoji/emoji@1.0" for Emoji

// ❌ Avoid: unknown source, unpinned, unverified
import "gh:random-user/suspicious-lib/module" for SomeClass

Multiple Versions#

import "gh:user/lib/module" for Module as ModuleLatest
import "gh:user/lib/module@v1.0" for Module as ModuleV1

// Use specific version based on your needs
var result = ModuleV1.someFunction()

Troubleshooting#

Module not found:

  • Visit the GitHub URL in a browser to confirm the path exists.

  • Check your internet connection (required for first-time downloads).

  • Confirm the branch name — defaults to main, not master.

Import fails silently:

  • Check the Bialet server logs.

  • Confirm the module returns raw Wren code, not HTML or an error page.

  • Try the full URL format to isolate GitHub-shorthand issues.

Cached version is outdated:

DELETE FROM BIALET_REMOTE_MODULES WHERE module LIKE 'gh:user/repo%';

Then restart your Bialet application. Prefer version tags over branches going forward.

“Import type not supported”:

  • Use gh:, http://, or https:// only — no ftp://, no file://.

  • Check for typos in the import statement.

Performance Tips#

  1. Use version tags to avoid unnecessary cache invalidation.

  2. Import only what you need — every import adds to first-load time.

  3. Pre-cache modules in development before deploying.

  4. Monitor cache size if you import many large modules.

Key Takeaways#

  • gh: shorthand or full URLs — both cached in BIALET_REMOTE_MODULES, never auto-updating once cached.

  • Write small helpers locally — a one-line function doesn’t justify a remote dependency; reserve external imports for substantial libraries.

  • Relative imports inside a remote module don’t work — a module’s own internal imports must use full gh:/URL syntax pointing at itself.

  • You can force a refresh by deleting the module’s row from BIALET_REMOTE_MODULES via a normal Query, but that trades away the stability guarantee — only do it for libraries you own.