Scripting · Reusable code
Imports & exports.
import(path) executes an external Nift script in an isolated scope and imports only the bindings that script explicitly exports.
Use bare import(path) in script land: standalone .f files, package source, the REPL and @script blocks. At template top level, use the @import(path) directive. Script land continues to accept @import(path) for compatibility, but bare import(path) is the canonical spelling.
// scripts/math.f
factor := 2
double := x => x * factor
export(double)
@import("scripts/math.f")
<p>$[double(21)]</p>
The imported script cannot implicitly read bindings from its caller. export(name) exports the actual binding, not a value snapshot, so exported closures retain private state correctly. Closures capture script locals (use a lambda such as double := x => x * factor); named fn declarations resolve free names in the calling scope, matching ordinary template @fn semantics.
count := 0
increment := () => ++count
export(count)
export(increment)
Exports are validated and installed atomically. Exporting a missing name, exporting the same name twice, or colliding with an existing caller binding makes the import fail without partially changing the caller.
Completion
| Completion | Meaning during an import |
| Fall through | Successful import; commit valid exports. |
return | Early successful completion; commit valid exports. |
return value | Error. Imports communicate programmatically through export(). |
Imported scripts participate in Nift's path-security and tracked dependency model, including applicable transitive imports.
Recoverable import failures
A missing or unreadable import source is a recoverable operational failure that try/catch can handle. The codes are io.import_source_unreadable for an ordinary non-package import, and package.import_source_unreadable / package.not_installed for package imports. Rollback happens before the caller's catch runs, so no partial exports or types are visible.
try {
import("./optional-module.f")
} catch(err) {
print(err.code) // io.import_source_unreadable
}
Malformed import syntax, invalid argument types, import cycles, malformed package metadata, authority/path violations, and failed imports that created a worker remain fatal and bypass catch.
Git-native packages
Nift packages are ordinary Git repositories containing a manifest.json and Nift .f modules. Install an official registry package with nift add sqlite, an arbitrary GitHub package with nift add github:user/repo, or a local development package with nift add ../package.
nift add sqlite
nift add github:user/package --ref=latest-tag
nift install
nift update sqlite
nift remove sqlite
Package references may track latest, latest-tag, an explicit tag/branch/commit, or a local path. Remote dependencies are recorded in .nift/packages.lock.json at an exact Git commit so normal installs are reproducible; nift update deliberately advances floating references.
Dependencies are transitive: installing a package installs its declared dependencies too, and packages.lock.json records the complete resolved graph (every package node with its exact commit plus the requirement edges). nift packages [name] [--json] explains why a package is installed, listing every dependency path and what each parent requested.
An installed package is imported by its package name:
import("sqlite")
db := sqlite.open("content.db")
rows := sqlite.query(db, "SELECT * FROM posts")
Module-style exports
A package can export a struct instance or plain object whose callable fields are invoked as methods, giving packages a natural vips.resize(...)-style interface while keeping package-private helpers out of the importer. Callable fields retain access to the module's private helpers; those helpers and any other private bindings never leak into the importing project. In package .f source the helpers use ordinary script-land fn syntax.
fn(scale_helper(x)) { return x * 2 }
@struct(vips_lib) { resize := (w, h) => { return {"w": scale_helper(w), "h": scale_helper(h)} } }
vips := vips_lib()
export(vips)
import("vips")
r := vips.resize(100, 50)
print(r.w) // 200
Source ownership
Relative imports resolve from their defining source, not the process working directory: a file that imports ./helper.f finds helper.f next to that file, even when the file is later invoked through a function, callback, lambda or worker. Missing siblings are errors and never fall through to a same-named file in the consuming project.
module_path() returns the absolute directory of the currently executing file-backed module; package_path() returns the absolute root of the package that owns the current module. Both accept one optional relative path argument and are confined to their owner (package-owned results never escape the package root). They only produce path strings and never fall back to the working directory.
Installed packages carry frozen lock provenance (canonical source + exact commit or local). A package whose remote generation changed since it was imported is a controlled stale-provenance error; an unchanged local package continues to follow its live local contents.