gulp is a MoonBit-native build tool inspired by the Node.js gulp task runner.
This repository slice currently contains:
i5ting/gulp/core: task registration, task lookup,series,parallel,lastRun, and registry tree outputi5ting/gulp/stream: byte streams, file streams, zero-to-many transforms, and in-memorysrc/dest, plus native file-backedByteStreamandfile_destfor files, empty files, directories, symlinks, filesystem metadata, and named stream plugins; context-aware file byte streams close native readers on cancellationi5ting/gulp/vinyl: a MoonBit Vinyl file model with contents, path history, stat kind, symlink target, custom metadata, clone with replayable stream contents, base/cwd proxy behavior, and path-derived helpersi5ting/gulp/through2: typed MoonBit-native through2-style transforms forByteStream,FileStream, andVinylFileStream, covering passthrough, map/filter/flatMap, flush, error propagation, and reusable factoriesi5ting/gulp/cli: pure config parsing, CLI argument selection, task list/tree rendering, andmoon runproxy command modelingi5ting/gulp/entry: the task-entry convention for user build packagesi5ting/gulp/platform: in-memory filesystem behavior for platform-layer tests, including file reads/writes, directory creation, deletion, stat, path normalization, and nativemoon runprocess execution with stdout/stderr capture, clocks, signal polling, cancellation bridging, glob expansion, and lazy glob discovery, plus glob watch snapshots for added/removed path events and explicit macOS/Linux/Windows path behavior testsi5ting/gulp/cmd/gulp: native executable entry package with a minimal command surface, real upwardgulp.mbtxdiscovery, and explicit script path loading that proxies default and explicit tasks intomoon runcommands and can list/render configured tasks with structured command statusi5ting/gulp/examples/basic: a minimalgulp.mbtxtask script
Full filesystem-backed src, remaining CLI actions, watch mode, and broader OS
backends have their own implementation slices.
This slice can be used from MoonBit package tests and the native gulp
command package. The native command reads gulp.mbtx from the real filesystem
and dispatches configured tasks through moon run.
Create a gulp.mbtx file at your project root:
///|
import {
"moonbitlang/core/env" @env,
"i5ting/gulp/entry" @entry,
}
///|
fn build(_ctx : @core.Context) -> Result[Unit, @core.MulpError] {
println("building...")
Ok(())
}
///|
fn main {
@entry.run_tasks([("build", build)], @env.args(), default_task="build")
}Then run:
gulp
gulp build
gulp --tasks
gulp --tasks-simple
gulp --tree build
gulp --watch buildgulp searches upward from the current directory for gulp.mbtx. You can also
point to a script explicitly:
gulp --gulpfile path/to/gulp.mbtx build
gulp --file path/to/gulp.mbtx --tasks
gulp --gulpfile path/to/gulp.mbtx --tree buildFor local repository development, run the native command package directly:
moon run cmd/gulp --target native -- --cwd examples/basic buildBuild the native executable in release mode:
cd gulp
moon build cmd/gulp --release --target nativeThe release binary is emitted at:
gulp/_build/native/release/build/cmd/gulp/gulp.exe
Install it by copying that file to a directory on PATH, renaming it to gulp
on macOS/Linux if desired:
mkdir -p ~/.local/bin
cp gulp/_build/native/release/build/cmd/gulp/gulp.exe ~/.local/bin/gulp
chmod +x ~/.local/bin/gulp
export PATH="$HOME/.local/bin:$PATH"Run the release smoke test to verify the binary can be found through PATH and
can execute the basic example end to end:
node gulp/scripts/release_smoke.mjsThe CLI validates gulp flags, then proxies the remaining task arguments to:
moon run --target native <gulp.mbtx> -- <args>Bare discovery does not fall back to gulp.mbt or gulp.toml. Legacy
gulp.mbt metadata can still be loaded through an explicit path for
compatibility tests.
The native command keeps its status source explicit:
| Case | Status | Stream |
|---|---|---|
--help, --version, -v, successful task, successful --tasks / --tasks-simple / -T / --tree |
0 |
stdout |
CLI usage error, missing gulp.mbtx, unreadable config, malformed legacy config |
1 |
stderr |
Unknown task reported by gulp.mbtx |
script status, usually 1 |
script stderr |
--watch <task> where the script task fails |
script status, usually 1 |
script stderr |
Explicit legacy entry or moon run process failure |
child process status | child stdout/stderr |
Register tasks in a Registry, then compose them with series or parallel:
let registry = new_registry()
let build = task("build", fn(ctx) -> Result[Unit, MulpError] {
if ctx.is_cancelled() {
Err(task_failed("build", "cancelled"))
} else {
Ok(())
}
})
registry.register(build).unwrap()
let pipeline = series([build])
pipeline.run(new_context(
cwd="/workspace",
now_ms=0L,
cancellation=new_cancellation_token(),
))Use src to create an in-memory FileStream, compose zero-to-many transforms
with pipe, and terminate with dest:
let input = file(
cwd="/workspace",
base="/workspace/src",
path="/workspace/src/app.txt",
contents=Bytes(byte_stream(["hello"])),
)
let sink = memory_dest()
let pipeline = src([input])
.pipe(fn(source : File) -> FileStream {
let copy = file(
cwd=source.cwd,
base=source.base,
path=source.path + ".copy",
contents=source.contents,
)
file_stream([source, copy])
})
.pipe(dest("dist", sink))
let ctx = new_context(
cwd="/workspace",
now_ms=0L,
cancellation=new_cancellation_token(),
)
let outputs = pipeline.collect(ctx).unwrap()dest records output files and byte contents in MemoryDest:
inspect(outputs.length(), content="2")
inspect(sink.contents_for("dist/app.txt").unwrap(), content="hello")File::relative_path() preserves paths under base and falls back to the full
path for files outside that base. Use Text("...") for text contents,
Buffer(bytes) for native byte buffers, and Bytes(stream) for streaming byte
contents. ByteStream reads Bytes chunks and is single-consumption, so a
file's byte contents should be read by one terminal consumer. Use
byte_stream_from_bytes for already-buffered bytes, read_all_bytes(max_bytes=...)
when raw bytes are needed, and read_all(max_bytes=...) when a bounded text
string is needed.
For plugin logic that would use through2 in gulp, prefer
i5ting/gulp/through2 and pipe typed transforms directly:
let rename = @through2.object_transform_files(
map=Some(fn(file : @stream.File) -> Result[@stream.File?, @core.MulpError] {
Ok(Some(file.with_path(file.path + ".out")))
}),
)
let output = @stream.file_stream([input]).pipe_stream(rename)Named plugins are wrappers around zero-to-many transforms and can be piped like regular transforms when plugin metadata is useful:
let copy_plugin = plugin("copy", fn(file : File) -> FileStream {
file_stream([file])
})
let output = src([input]).pipe_plugin(copy_plugin)Transforms and plugins can return error_stream(err) to propagate failures
through downstream collection or destinations.
Use file_byte_stream_with_context when a native file reader should close as
soon as its task context is cancelled.
Native file_dest writes byte streams chunk by chunk, so outputs larger than the
in-memory read_all limit do not need to be buffered as one string.
Use merge_sources([a, b]) to concatenate multiple source streams while
preserving stream order and error propagation.
filter_files, map_files, flat_map_files, and i5ting/gulp/through2
cover the common zero/one/many transform helpers used by gulp-style plugins.
Use concat_plugin("bundle.txt") as a stream-level plugin when multiple
buffered input files should become one output file:
let bundle = src([a, b]).pipe_stream(concat_plugin("bundle.txt"))Native async task integration lives in i5ting/gulp/stream_async. Its
async_task_from_stream(name, stream, signals=...) adapter drains a FileStream
and uses the drain result as the task completion signal. The adapter registers
that stream completion signal with the shared completion validator, so tasks
that also declare callback or process completion fail before draining.
In i5ting/gulp/core, new_async_registry(), async_task(...),
async_task_from(...), async_series(...), and async_parallel(...) provide
the native async task graph. i5ting/gulp/entry mirrors those helpers with
new_async_registry(), async_task(...), async_task_from(...),
async_run_args(...), render_async_args(...), and
async_run_entry_args(...) so custom entry wiring can run async tasks, render
task lists/trees, and fall back to the default build task without a hand-written
argument dispatcher.
In i5ting/gulp/core, async_task_from_callback(name, register) supports an explicit
callback-style completion protocol, waits until the callback is called, and
rejects duplicate callbacks.
validate_async_completion_signals(name, signals) rejects tasks that combine
completion modes, such as returning a stream while also calling a callback.
Native child process task integration lives in i5ting/gulp/platform_async.
async_task_from_process(name, command) spawns a child process, awaits its exit
code, and cancels the child when the shared task context is cancelled.
async_watch_loop(...) connects normalized glob watch events to a debounced
async loop, runs the current handler without blocking event polling, queues a
pending rebuild when changes arrive mid-run, and exits cleanly when the shared
context is cancelled.
async_file_worker_pool(concurrency=...) provides bounded file-level async
work for future AsyncFileStream integrations: it pulls from upstream only
after a worker slot is available, so file processing can be parallelized without
unboundedly reading the stream ahead.
Task concurrency and stream concurrency are separate layers. series and
parallel schedule whole tasks; a task that drains a FileStream still observes
the stream's pull-based backpressure and processes files through the current
sync transform chain. File-level async transforms build on the bounded
stream_async worker pool so task-level parallelism does not accidentally
create unbounded file work.
Stream benchmarks cover buffered pipeline throughput and large byte-stream
file_dest writes:
moon bench --package stream --target nativeVinyl and vinyl-fs benchmarks are native-only. They cover Vinyl construction,
relative path access, cloning, buffered src, buffered dest, and streaming
dest:
moon bench --package vinyl --target native
moon bench --package vinyl_fs --target nativeUse the comparison harness to run the same release-native gulp scenarios next to local JS scenarios with repeated medians. The harness exits non-zero when MoonBit native is slower than JS for any selected scenario:
node gulp/scripts/bench_compare.mjs
node gulp/scripts/bench_compare.mjs --json
node gulp/scripts/bench_compare.mjs --scenario vinyl-fs-src-buffer-1000 --rounds 3
node gulp/scripts/bench_compare.mjs --scenario pipeline-memory-dest --rounds 3gulp.mbtx is the user-facing task entry. The CLI discovers it upward from the
current directory or accepts an explicit path through --config, --gulpfile,
--file, or --gulpfile, then proxies the original task arguments to MoonBit:
let command = moon_run_script_command("/workspace/app/gulp.mbtx", ["build"])command.executable is "moon" and command.args is
["run", "--target", "native", "/workspace/app/gulp.mbtx", "--", "build"].
Legacy gulp.mbt metadata files can still be loaded through an explicit path
for compatibility tests, where parse_config and moon_run_command infer or
read the older entry package shape.
For in-process task listing and tree rendering, render output from a Registry:
let listing = render_cli_output(
parse_args(["--tasks"], default_task="build").unwrap(),
registry,
).unwrap()To test config discovery and command dispatch without platform IO, pass a virtual set of config files:
let result = dispatch_cli(
cwd="/workspace/app/src",
files=[("/workspace/app/gulp.mbtx", "fn main { println(\"build\") }")],
args=["build"],
registry=registry,
).unwrap()The native command package discovers a real gulp.mbtx upward from the current
directory, supports --config, --gulpfile, --file, --tasks, and --tree,
and runs task scripts as:
moon run --target native <gulp.mbtx> -- <task>For example, this gulp.mbtx dispatches gulp build directly:
import {
"moonbitlang/core/env" @env
}
fn selected_task(args : Array[String]) -> String {
if args.length() >= 2 {
args[1]
} else {
"build"
}
}
fn main {
println("running " + selected_task(@env.args()))
}Discovery does not automatically fall back to legacy config files. Projects
that still need the legacy parser can pass an explicit path with --config,
--gulpfile, or --file; bare gulp searches for gulp.mbtx only.
A minimal project keeps gulp.mbtx at the workspace root:
///|
import {
"moonbitlang/core/env" @env,
"i5ting/gulp/entry" @entry,
}
///|
fn build(_ctx : @core.Context) -> Result[Unit, @core.MulpError] { Ok(()) }
fn clean(_ctx : @core.Context) -> Result[Unit, @core.MulpError] { Ok(()) }
fn watch(_ctx : @core.Context) -> Result[Unit, @core.MulpError] { Ok(()) }
fn parallel_task(_ctx : @core.Context) -> Result[Unit, @core.MulpError] { Ok(()) }
///|
fn main {
@entry.run_tasks(
[("build", build), ("clean", clean), ("watch", watch), ("parallel", parallel_task)],
@env.args(),
)
}A file pipeline follows gulp's src().pipe(...).pipe(dest()) shape:
let pipeline = @stream.src([input])
.pipe(@stream.map_files(fn(file : @stream.File) -> @stream.File {
{
cwd: file.cwd,
base: file.base,
path: file.path + ".copy",
contents: file.contents,
metadata: file.metadata,
}
}))
.pipe(@stream.file_dest("dist"))For series/parallel composition, use the registry API to get task handles:
fn main {
let registry = @entry.new_registry()
let clean = @entry.task(registry, "clean", fn(_ctx) -> Result[Unit, @core.MulpError] {
Ok(())
})
let build = @entry.task(registry, "build", fn(_ctx) -> Result[Unit, @core.MulpError] {
Ok(())
})
let all = @core.series([clean, build])
ignore(@core.task(registry, "default", fn(ctx) { all.run(ctx) }))
@entry.run(registry, @env.args())
}Watch is invoked through the CLI:
gulp --watch buildCurrent watch support proxies the watch request to the script and runs the task once; the long-running native filesystem watcher is tracked in the remaining watch TODOs.
Key differences from gulp today:
gulp.mbtxis MoonBit source, not JavaScript.- Node gulp plugins cannot be reused directly.
- Streams are native MoonBit
FileStream/ByteStreamvalues. - Bare discovery searches for
gulp.mbtx; legacy config files require an explicit path.
Move the task entry from JavaScript exports to one gulp.mbtx script at the
project root. A gulp task exported as exports.clean = clean becomes a branch
in the script dispatcher, and the default export becomes the fallback task when
no CLI task name is provided.
exports.clean = clean;
exports.build = gulp.series(clean, scripts);
exports.default = exports.build;///|
import {
"moonbitlang/core/env" @env,
"i5ting/gulp/entry" @entry,
}
///|
fn clean(_ctx : @core.Context) -> Result[Unit, @core.MulpError] { Ok(()) }
fn build(_ctx : @core.Context) -> Result[Unit, @core.MulpError] { Ok(()) }
///|
fn main {
@entry.run_tasks([("clean", clean), ("build", build)], @env.args())
}@entry.run_tasks handles --tasks, --tree, --tasks-json, and task dispatch
automatically — no hand-written argument dispatcher is needed. Replace
gulpfile.js, gulpfile.mjs, and transpiled gulpfiles with gulp.mbtx; the
CLI passes task arguments to moon run --target native <gulp.mbtx> -- <args>.
gulp --tasks-simple is accepted as a gulp-compatible alias and is normalized
to --tasks before invoking gulp.mbtx.
MoonBit stream helpers mirror gulp's src().pipe(...).pipe(dest()) shape in
the library packages. Standalone .mbtx scripts import those helpers through a
registry-resolvable module path, for example
"i5ting/gulp@0.1.0/entry" or "i5ting/gulp@0.1.0/stream".
For repository verification, scripts/test_mbtx_entry_parallel.mjs packages the
local module into an isolated registry cache, runs a real gulp.mbtx, and
checks that @entry.parallel([clean, scripts]) completes near one sleep.
- Create one
gulp.mbtxat the project root. - Move exported gulp tasks into named MoonBit functions and register them
with
@entry.run_tasks([("name", fn), ...], @env.args()). - Map
exports.defaultto thedefault_taskparameter ofrun_tasks. - Replace
gulp.series(...)andgulp.parallel(...)with@entry.series([...])and@entry.parallel([...]). - Replace
gulp.src(globs).pipe(plugin()).pipe(gulp.dest(outDir))with@vinyl_fs.src(...).pipe_stream(@through2.object_transform_vinyl(...)).pipe(@vinyl_fs.dest(...))for Vinyl pipelines, or@stream.file_stream(...).pipe_stream(@through2.object_transform_files(...))for lower-level file pipelines. - Replace Node callback tasks with
@core.async_task_from_callback(...). - Replace
gulp.watch(globs, task)withgulp --watch taskfor CLI workflows or@platform_async.async_watch_loop(...)for embedded watch loops. - Run
gulp --tasksandgulp --tree buildafter migration to verify the task registry output matches the old gulp CLI shape.
gulp is intentionally MoonBit-native and does not execute JavaScript
gulpfiles. gulpfile.js, gulpfile.mjs, Babel/TypeScript-transpiled gulpfiles,
and npm gulp plugins must be migrated to gulp.mbtx, MoonBit transforms, or
external process tasks.
Node stream plugins are not loaded directly. Port the transform to MoonBit when it is part of the file pipeline, or wrap tools such as TypeScript, Rollup, or MoonBit builds as process tasks. The plugin examples package contains simple rename/filter/concat transforms and process wrappers for TypeScript and MoonBit build commands.
Native OS watch backends are still being filled in. The async watch loop already handles debounce, pending rebuilds, cancellation, and failure recovery, while the current native watcher uses a polling snapshot facade until macOS, Linux, and Windows event backends are implemented.
| gulp recipe | gulp shape |
|---|---|
gulp.src(globs).pipe(plugin()).pipe(gulp.dest(outDir)) |
Use @vinyl_fs.src(...), @through2.object_transform_vinyl(...), and @vinyl_fs.dest(...). |
gulp.series(clean, gulp.parallel(styles, scripts)) |
Register clean, styles, and scripts, then expose @entry.series([clean, @entry.parallel([styles, scripts])]). |
gulp.watch(globs, task) |
Use gulp --watch task from the CLI, or wire @platform_async.async_watch_loop(...) when embedding a watcher. |
gulp.src(globs, { since: gulp.lastRun(task) }) |
Use @core.last_run(...) / @core.last_run_async(...) with src(..., since=...). |
Callback task function(done) { ...; done(err) } |
Use @core.async_task_from_callback(...); duplicate completion is rejected. |
| Stream-returning task | Use @stream_async.async_task_from_stream(...) so the task completes when the stream drains or fails. |
gulp --tasks-simple |
Use gulp --tasks-simple; it is normalized to --tasks for gulp.mbtx scripts. |
gulp --tasks / gulp --tree |
Handled automatically by @entry.run_tasks(...) — no explicit dispatcher needed. |
| gulp CLI flag or behavior | gulp status | Notes |
|---|---|---|
gulp default task |
Supported as gulp |
The selected default is defined by the gulp.mbtx dispatcher. |
gulp <task> |
Supported as gulp <task> |
One task name is accepted by the native CLI and forwarded to gulp.mbtx. |
gulp <task> <task> concurrent CLI tasks |
Not supported yet | Use an explicit @entry.parallel([...]) task in gulp.mbtx. |
-v / --version |
Supported | Prints the native gulp version. |
--cwd <dir> |
Supported | Discovery and command execution use the supplied working directory. |
--gulpfile <path> |
MoonBit-compatible alias | Points to gulp.mbtx or explicit legacy gulp.mbt; it does not execute JS gulpfiles. |
--file <path> / --gulpfile <path> |
Supported | --gulpfile is the preferred native spelling. |
--tasks-simple |
Supported | Normalized to --tasks before script execution. |
--tasks |
Supported with gulp semantics | gulp expects the script to print the task list; gulp's decorated tree output is not cloned. |
-T |
Supported as an alias for --tasks |
It lists tasks with current gulp semantics rather than gulp's decorated task tree. |
--require <module> |
Not supported | gulp.mbtx is MoonBit script mode, not a JS/transpiled gulpfile. |
--verify |
Not supported | npm plugin blacklist verification does not apply to native MoonBit plugins. |
--color / --no-color / --silent |
Not supported yet | Current output is plain stdout/stderr from the script and native command. |
gulp accepts several task completion shapes:
exports.build = gulp.series(clean, gulp.parallel(styles, scripts));
exports.assets = function assets() {
return src("src/**/*").pipe(dest("dist"));
};
exports.deploy = function deploy(done) {
upload(function (err) {
done(err);
});
};In i5ting/gulp/core, the native async equivalents are explicit task values. Use
@entry.series([clean, build]) and @entry.parallel([styles, scripts]) for
task-level ordering and concurrency, wrap callback-style work with
@core.async_task_from_callback, and wrap stream completion with
@stream_async.async_task_from_stream.
let clean = @entry.async_task(registry, "clean", fn(_ctx) -> Result[Unit, @core.MulpError] {
Ok(())
})
let styles = @entry.async_task(registry, "styles", fn(_ctx) -> Result[Unit, @core.MulpError] {
Ok(())
})
let scripts = @entry.async_task(registry, "scripts", fn(_ctx) -> Result[Unit, @core.MulpError] {
Ok(())
})
ignore(@entry.async_task_from(
registry,
"build",
@entry.series([clean, @entry.parallel([styles, scripts])]),
))Stream-returning gulp tasks map to a task whose completion is the stream drain:
let assets = @stream_async.async_task_from_stream(
"assets",
@stream.src(files).pipe(@stream.dest("dist", sink)),
)Callback-style gulp tasks map to a callback adapter that waits until the callback fires and rejects duplicate completion:
let deploy = @core.async_task_from_callback("deploy", async fn(done) -> Unit {
@async.sleep(10)
done(Ok(()))
})The completion validator rejects mixed modes such as a task that returns a stream and also declares callback or process completion. This mirrors gulp's rule that a task should use one completion signal.
gulp does not load npm gulp plugins directly. gulp plugins are JavaScript
Transform streams that run inside Node, while gulp plugins are MoonBit
transforms over native FileStream and ByteStream values. Port plugin logic
to a MoonBit transform, wrap an external tool as a process task, or keep that
part of the pipeline in gulp until a dedicated adapter exists.
For plugin logic that was previously written with through2, prefer
i5ting/gulp/through2. It is not a JavaScript API clone: it exposes typed
MoonBit transforms (transform_bytes, object_transform_files,
object_transform_vinyl, lower-level through_files / through_vinyl, and
fresh-state ctor_* factories) instead of Node callback, this.push, and
TransformOptions objects.
Common through2 callback patterns map to explicit Result values:
| JS through2 pattern | MoonBit through2 shape |
|---|---|
through2() passthrough |
@through2.transform_bytes() |
through2.obj(fn) |
@through2.object_transform_files(map=Some(fn)) or @through2.object_transform_vinyl(map=Some(fn)) |
through2.ctor(fn) |
@through2.ctor_files(fn) / @through2.ctor_vinyl(fn) / @through2.ctor_bytes(fn) |
cb(null, chunk) |
Ok(Some(chunk)) |
| no output / filtered chunk | Ok(None) |
this.push(a); this.push(b); cb() |
Ok([a, b]) via *_flat helpers |
_flush(cb) |
flush=Some(fn() -> Result[Array[_], @core.MulpError] { ... }) |
cb(err) / thrown error |
Err(@core.stream_error("message")) |
| gulp API | gulp API | Notes |
|---|---|---|
src(globs, options) |
@vinyl_fs.src(globs, @vinyl_fs.src_options(...)) or lower-level @stream.src(files) |
vinyl_fs reads the native filesystem and preserves Vinyl-style cwd, base, relative, metadata, buffered, and streaming contents. allow_empty, remove_bom, encoding=false, and sourcemaps=true map to gulp's allowEmpty, removeBOM, raw byte, and adjacent .map loading behavior. |
dest(outDir, options) |
@vinyl_fs.dest(outDir) / @vinyl_fs.dest_with_options(...) or lower-level @stream.file_dest(...) |
Writes real files and returns files with updated destination paths. mode, dir_mode, and mtime_ms metadata are applied to native filesystem entries; overwrite=false, append=true, external sourcemaps=Some("."), and inline_sourcemaps=true cover the matching vinyl-fs write modes. |
symlink(outDir, options) |
@vinyl_fs.symlink(outDir) / @vinyl_fs.symlink_with_options(...) |
Emits filesystem symlinks where the native platform supports them; relative_symlinks=true maps to vinyl-fs relativeSymlinks, and overwrite=false preserves an existing destination link. |
new Vinyl({ sourceMap }) |
@vinyl.vinyl_file(source_map=...) |
MoonBit keeps custom JS-style props in typed fields or metadata; source_map is first class. |
task(name, fn) |
@entry.run_tasks([("name", fn), ...], args) — or @entry.task(registry, ...) when task handles are needed for series/parallel. |
run_tasks is the primary pattern for gulp.mbtx. |
series(...) |
@entry.series([...]) / @core.series([...]) |
Preserves left-to-right task order. |
parallel(...) |
@entry.parallel([...]) / @core.async_parallel([...]) |
Native async tasks run concurrently with shared cancellation. |
watch(globs, task) |
@platform_async.async_watch_loop(...) plus CLI watch dispatch |
Event normalization and pending rebuild behavior are implemented; native OS event backends are still a TODO. |
lastRun(task) |
@core.last_run(...) / @core.last_run_async(...) |
Integrates with src(..., since=...) style filtering. |
| callback completion | @core.async_task_from_callback(...) |
Duplicate completion and mixed completion modes are rejected. |
| stream completion | @stream_async.async_task_from_stream(...) |
A stream task completes when the stream drains or fails. |
gulp.toml was an entry metadata file that pointed at a separate MoonBit entry
package. gulp.mbtx replaces that model with a single script-mode entry file.
Use this mapping:
gulp.toml entry package -> gulp.mbtx script
gulp.toml task metadata -> --tasks / --tree output from gulp.mbtx
moon run <entry> -- <task> -> moon run --target native <gulp.mbtx> -- <task>
Bare discovery now searches upward only for gulp.mbtx. It does not
automatically fall back to gulp.toml or legacy gulp.mbt metadata. Existing
compatibility tests can still pass an explicit path with --config,
--gulpfile, or --file, but new projects should create gulp.mbtx and put
their task dispatch there.
The older explicit gulp.mbt compatibility path still supports a MoonBit build
entry package. That package registers tasks by exposing a function that
matches @entry.RegisterTasks. By convention, name it register_tasks; it
receives a Registry owned by the entry runtime and adds task definitions to it:
pub fn register_tasks(registry : @core.Registry) -> Unit {
ignore(@entry.task(registry, "build", fn(_ctx) -> Result[Unit, @core.MulpError] {
Ok(())
}))
}Later entry helpers will create the registry, call this function, and use the registered tasks for CLI execution and task list/tree output.
For tests or custom entry wiring, create the registry through the entry helper:
let registry = @entry.new_registry()
register_tasks(registry)An entry main can pass @env.args() to run_args; the helper runs the task
name supplied by moon run <entry> -- <task>:
fn main {
let registry = @entry.new_registry()
register_tasks(registry)
let ctx = @core.new_context(
cwd=".",
now_ms=0L,
cancellation=@core.new_cancellation_token(),
)
@entry.run_args(registry, @env.args(), ctx).unwrap()
}Use render_args for entry-local task discovery output:
let output = @entry.render_args(registry, ["build", "--tasks"]).unwrap()Async entry wiring uses the async registry and async run helper:
async fn main {
let registry = @entry.new_async_registry()
let clean = @entry.async_task(registry, "clean", fn(_ctx) -> Result[Unit, @core.MulpError] {
Ok(())
})
let scripts = @entry.async_task(registry, "scripts", fn(_ctx) -> Result[Unit, @core.MulpError] {
Ok(())
})
ignore(@entry.async_task_from(registry, "build", @core.async_parallel([clean, scripts])))
let ctx = @core.new_context(
cwd=".",
now_ms=0L,
cancellation=@core.new_cancellation_token(),
)
@entry.async_run_entry_args(registry, @env.args(), ctx, fn(output) {
println(output)
}).unwrap()
}See examples/basic for a minimal gulp.mbtx task script.
The CLI accepts gulp --watch build and proxies it to
moon run --target native <gulp.mbtx> -- --watch build; the current script runs
the named task once. The platform async watch loop already supports debounce,
mid-run pending rebuilds, and rerun after completion, but the CLI still needs to
wire that loop to a long-running native filesystem watcher.
At the platform layer, watch_glob(paths, pattern) keeps a matched-path
snapshot and poll(next_paths) reports added/removed matching paths.
debouncer(delay_ms) can then coalesce repeated watch events until the quiet
window has elapsed.
watch_run_state() tracks whether a task is currently running and records a
pending rebuild when file changes arrive mid-run.
Task failures are recorded on the watch state without deactivating the watcher,
so later changes can still trigger another run.
Stopping the watch state cancels the active token, clears pending rebuilds, and
marks the watcher inactive.