Engine functions

#

One-shot calls, resolution helpers, the diagnostic renderers and the constants the engine publishes.

In this page:

ParseTargetSpec

#

Reads a target string - "es2020", "esnext", "firefox115", or a comma-separated list mixing both kinds - the same way the "--target" command-line flag does.

This lives beside the enums rather than in the command line because a config file names a target as a string too, and the two spellings have to mean the same thing. A caller that gets kUnknownName or kMissingVersion is told which part was at fault through "bad_value", so it can say so in its own terms.

ParseTargetSpec("es2020,firefox115", spec, bad) => kOk ParseTargetSpec("firefox", spec, bad) => kMissingVersion ParseTargetSpec("es2030", spec, bad) => kUnknownName

TargetParse ParseTargetSpec(std::string_view text, TargetSpec& out,
                            std::string_view* bad_value = nullptr);

ValidEngineNames

#

The engine names a target string may use, paired with their enum values. The keys are lowercase and the version is whatever follows the name, so a caller that has to render the list of valid targets - a diagnostic, a completion - builds it from here rather than from a second copy that could disagree.

const std::unordered_map<std::string, EngineName>& ValidEngineNames();

Build

#

Runs one complete build: resolve the entry points, walk the graph, compile and print everything, then tear the session down again. Nothing is written unless "write" is set.

This is the one-shot entry point. Reach for Context() instead when the caller intends to build more than once, since a build that is thrown away after use cannot be cancelled and re-runs its setup work every time.

Input: entry_points = {"src/index.js"}, bundle = true, format = kESModule, write = false, minify_whitespace = true Output: one OutputFile per chunk in "output_files", with "contents" holding the generated bytes; "errors" empty on success; "metafile" empty because "metafile" was not requested; the disk untouched.

Failures are reported, not thrown: a missing entry point comes back as result.errors[0] with a Message whose "location" points at the offending path.

BuildResult Build(const BuildOptions& options);

ResolveEffectiveBuildConfigs

#

Resolves "explicit_options" against the config file above "start_dir" and the built-in defaults, and returns what each of the project's builds should actually run with. This is the only implementation of that ranking: the command line, "dev", "watch", "serve", "clean" and every embedding of the C++ API go through it, so a project sees the same settings whichever way it is driven.

Every configuration is resolved against the same explicit options and the same defaults: an explicit CLI option overrides that field in all of them, and nothing one configuration says can leak into another.

Messages about a config file that exists but cannot be used - it could not be parsed, or two configurations write the same output - are returned in "config_invalid" rather than raised here, so a caller decides for itself whether that is an error or a warning. "builds" is empty when the config file could not be parsed; a collision leaves "builds" populated, so a caller that already has the request in hand can still fall back to it.

Input: explicit_options with outdir = "release" and bundle = true, and a guchho.config.json in "start_dir" that says outdir = "out" and minify = true Output: builds[0].options with outdir = "release" (explicit beats config), bundle = true, minify_whitespace = true (config beats the built-in default), and AbsOutputDir derived from any outfile

EffectiveBuildConfigs ResolveEffectiveBuildConfigs(const BuildOptions& explicit_options,
                                                   const std::string& start_dir);

Transform

#

Transforms one string of source code into JavaScript and returns the bytes without writing anything anywhere. This is the operation behind "compile this buffer", "minify this snippet" and "rewrite this file for the browser", and it is the same parser and printer the bundler uses, so a file that survives a transform will also survive being bundled.

Input: "const f = (x: number) => x * 2;" with loader = kTS, target = kES2015, minify_whitespace = true Output: code = "const f=x=>x*2;" (types erased, arrow preserved), errors empty, map empty because "sourcemap" was kNone.

A syntax error is reported the same way a build reports one: result.errors gets a Message with the "location" of the offending token and code stays empty. Nothing is thrown for bad input.

TransformResult Transform(const std::string& input, const TransformOptions& options);

Context

#

Creates a BuildContext from a set of options. Options are validated up front and the context is a working, ready-to-use object: the caller still decides when the first build happens.

Validation failures are returned rather than thrown, because they are ordinary user-facing problems: a bad option combination should be reported with a Message the caller can print, exactly like a compile error.

Input: entry_points = {"src/index.js"}, bundle = true, write = true Output: a context, or nullptr with "errors" filled in; for example a null context whose first error says "Must use "outdir" when there are multiple input files" if only "outfile" was set.

Input: an invalid LogLevel value cast to the enum Output: nullptr, with a message naming the option that failed to validate.

std::unique_ptr<BuildContext> Context(const BuildOptions& options, std::vector<Message>& errors);

FormatMessages

#

Renders diagnostics as text. The engine's own printing is optimised for a terminal it owns; this is the same rendering, returned as strings so another program can decide where they go - an editor's problems panel, a JSON response, a test's expected output.

One string comes back per message, in the order given, so the mapping back to the input vector is positional.

Input: one Message whose text is "Expected "}" but found end of file", location = { file = "src/app.js", line = 12, column = 4, line_text = " function main() {" }, with terminal_width = 0, color = false, kind = kError Output: a single string, of this shape: "✘ [ERROR] Expected "}" but found end of file\n" + "\n" + " src/app.js:12:4:\n" + " 12 │ function main() {\n" + " │ ^\n", with no escape sequences anywhere in it.

With color = true the same string carries ANSI escapes around the icon, the "[ERROR]" tag and the snippet, and the kind field decides whether that tag reads "ERROR" or "WARNING". A message with no location renders as the first line alone, and a message with notes gets each note appended as its own indented "NOTE" block.

std::vector<std::string> FormatMessages(
    const std::vector<Message>& msgs, const FormatMessagesOptions& options);

AnalyzeMetafile

#

Turns the metafile JSON produced by a build with "metafile" set into a human-readable size report. This is how a caller answers "why did my bundle grow" without writing a metafile parser of its own.

The report is a two-level table, largest output first: each output file is one row, and the inputs that contributed bytes to it are listed underneath it with their share of that output. Source maps are left out - the ".map" entries in the metafile describe the maps, not the program, and they would otherwise dominate the numbers.

Input: the metafile string from a BuildResult, with verbose = true Output: a table shaped like this (sizes are abbreviated, shares rounded):

        dist/app.js ─────── 241.3kb ── 100.0%
         ├ src/app.js ─────── 2.1kb ──  0.9%
         └ node_modules/x/index.js 239.2kb ─ 99.1%

      which reads as "almost all of dist/app.js is one dependency".

Input: text that is not a metafile, or a metafile with no usable outputs Output: an empty string - a bad document is not worth an exception, because the metafile is data the caller may have truncated or rewritten.

std::string AnalyzeMetafile(
    const std::string& metafile, const AnalyzeMetafileOptions& options);

strip_dir_prefix

#

Removes a leading directory from a path when the path really is inside that directory, and reports whether it did. The check is textual, so it works for absolute paths, for output paths that do not exist yet, and for the URL-style paths the server sees. Only a boundary counts as a match: a prefix that stops in the middle of a name is rejected, otherwise "/project" would wrongly match "/projectile". An empty prefix leaves the path alone, and a path equal to the prefix strips down to the empty string.

"separators" names the characters that separate path components, so a caller on a system with more than one kind passes both. Nothing is normalised here: a Windows path needs its backslashes listed, and the caller decides which form it wants back.

Input: path = "/project/src/app.js", prefix = "/project", separators = "/" Output: true, with out = "src/app.js" Input: path = "/project/src", prefix = "/project/", separators = "/" Output: true, with out = "src" (the prefix already ended in a separator) Input: path = "/projectile/app.js", prefix = "/project", separators = "/" Output: false, and out is cleared

bool strip_dir_prefix(const std::string& path, const std::string& prefix,
                      std::string_view separators, std::string& out);

validate_path_template

#

Splits an output-name template such as "chunks/[name]-[hash].js" into the segments the writer walks when it decides output names. Parsing happens once, up front, so the hot path is a sequence of cheap appends instead of repeated pattern matching.

Each resulting segment is "the literal text that came before a placeholder, together with the placeholder that follows it" - the placeholder is glued to the text it is appended after, and a trailing run of literal text with no placeholder after it becomes one final segment. The template is read as if it were written relative to "./", so the leading directory segment is always part of the first entry.

An empty template yields an empty vector, which the writer reads as "use the default name". A "[" that does not begin one of the four known placeholders is literal text, so a value that is not a template at all still round-trips.

Input: value = "chunks/[name]-[hash].js" Output: three segments, in order: { Data = "./chunks/", Placeholder = kName } { Data = "-", Placeholder = kHash } { Data = ".js", Placeholder = kNoPlaceholder }

Input: value = "bundle.js" (no placeholders) Output: { Data = "./bundle.js", Placeholder = kNoPlaceholder }

Input: value = "" (nothing was configured) Output: an empty vector.

std::vector<config::PathTemplate> validate_path_template(std::string_view value);

Serve

#

Starts the development server and returns as soon as it is accepting connections. This is the primitive that BuildContext::Serve is built on, and it can be used directly when the caller wants the server without a build context - a proxy in front of another bundler, for instance, or a test that needs a real socket.

The socket is bound before this returns, so the port in the result is final and the first request will not lose a race. Requests are then served on background threads until result.stop() closes the listener and the open event streams. At log_level kInfo the reachable URLs are printed as soon as the server is up, which is the only output this function produces itself.

Failures are reported through "error" rather than thrown, matching the rest of the API: a port that cannot be bound, a half-specified TLS pair, a servedir or fallback that cannot be turned into an absolute path, or a CORS origin with more than one "*" in it all come back as a message and an empty ServeResult. The servedir and fallback are made absolute against the filesystem's working directory, so a relative value means the same thing here as it would in the build that produced the files.

Asking for port 0 does not mean "any free port" straight away: the 8000-8009 range is tried first, so a run finds the same port again instead of changing every time, and only then does the operating system pick one for us.

Input: rebuild = a BuildContext's Rebuild(), options = { port = 0, servedir = "dist" }, log_level = kInfo, use_color = kColorIfTerminal Output: a ServeResult with the port that was bound and a stop callback, and nothing in "error". The caller must eventually invoke result.stop() to release the socket and the worker threads.

The in-repo server speaks plain HTTP only: providing keyfile and certfile is rejected as unsupported, and providing just one of the two is a different error again, because the pair is checked before the feature is refused.

ServeResult Serve(
    filesystem::Fs&       fs,
    RebuildFn             rebuild,
    const ServeOptions&   options,
    logger::LogLevel      log_level,
    logger::UseColor      use_color,
    std::string&          error);

RebuildFn

#

How the server obtains output for a request. The server calls it for every request that needs the latest build, and the function is expected to return either the result of the build that is already running or a freshly completed one. In practice this is a context's Rebuild(), which is why asking for a page in a browser after saving a file returns the new output without any extra bookkeeping: the server itself decides when a rebuild is needed.

using RebuildFn = std::function<BuildResult()>;

Edit this page on GitHub