中文

Micro applications

Use func/parser to prototype small CLIs.

Updated

The full func application runtime is already compact enough for most projects. func/parser is a better fit when you are prototyping in a Node.js-compatible environment with the smallest practical footprint, or when the program is simple enough that learning the application model would be premature.

It works especially well when a short block of imperative code can express the entire CLI. Package initializers, repository-maintenance scripts, and thin wrappers around another program often need only a few options followed by one business function call.

func/parser is a separate typed argument-parsing entry for these applications. It reuses func’s core argv compilation and parsing capabilities, but does not load the full decorator-based application model or run a command. After parse() returns data, the application still owns branching, help, output, exit codes, and business execution.

When it fits

func/parser is usually the direct choice when most of these statements are true:

  • The CLI has root options or a small number of one-level commands.
  • Options need only Boolean, String, and a very small argument surface.
  • Positional inputs can be passed to business code as a string array without declarative validation.
  • Help is short enough to maintain manually without drift.
  • Business dependencies can be imported directly without injection or resource lifecycle management.

Typical examples include create-* initializers, single-purpose generators, CI helpers, Git hook entry points, and process wrappers that add a thin layer of defaults.

If the project already needs nested commands, shared services, structured help, centralized errors, or invocation-level testing, starting with the full func model avoids modeling the same interface twice.

Define and call a parser

Import createParser from func/parser and provide root options and an optional command map. The parser infers the result type from that definition.

src/index.ts
import { ParserError, createParser } from 'func/parser'

const parser = createParser({
  options: {
    help: { alias: 'h', type: Boolean },
  },
  commands: {
    build: {
      options: {
        outDir: { alias: 'o', name: 'out-dir', type: String },
        tags: { alias: 't', multiple: true, name: 'tag', type: String },
      },
    },
  },
})

export const main = async (argv = process.argv.slice(2)) => {
  try {
    const invocation = parser.parse(argv)
    if (invocation.options.help) {
      printHelp(invocation.command)
      return
    }

    if (invocation.command === 'build') {
      await build({
        entries: invocation.inputs,
        outDir: invocation.options.outDir,
        tags: invocation.options.tags,
      })
      return
    }

    printHelp()
  } catch (error) {
    if (!(error instanceof ParserError)) throw error
    console.error(error.message)
    process.exitCode = 1
  }
}

The definition accepts calls such as:

tool --help
tool build entry.ts --out-dir dist --tag next --tag latest
tool build entry.ts -o dist -t next

parse() returns four fields:

FieldContents
commandThe selected one-level command name, or undefined.
inputsUnconsumed positional inputs in their original order.
optionsTyped values whose property names are the keys in the definition.
suppliedDefinition keys explicitly provided by the user, including negated flags.

Root options are visible to every command and may appear before or after the command name. Value options accept both --name value and --name=value; known short Boolean options may be grouped. Omitted Booleans are false, scalar values are undefined, and repeated values are empty arrays. Later scalar values win, while repeated values accumulate in input order.

Option definitions

Use constructors for the short form:

const parser = createParser({
  options: {
    color: Boolean,
    port: Number,
    profile: String,
  },
})

Use the object form to change the public name or parsing behavior:

PropertyPurpose
typeRequired; one of Boolean, String, or Number.
namePublic long name used by --<name>; defaults to the definition key.
aliasOne non-numeric, single-character short alias.
aliasesAdditional single-character aliases, merged and deduplicated with alias.
multipleString / Number only; collects repeated values into a readonly array.
negatableBoolean only; also accepts --no-<name>.

Unknown commands, unknown options, and invalid values throw ParserError. Its code is unknown-command, unknown-option, or invalid-argument, respectively. An invalid definition fails during createParser() with invalid-definition.

Wrap another program

With passthrough: true, the parser stops interpreting tokens at the first positional input after the command or at --. The remaining inputs can be forwarded to a child process:

src/runner.ts
import { createParser } from 'func/parser'

const parser = createParser({
  commands: {
    run: {
      options: {
        file: { alias: 'f', type: String },
      },
      passthrough: true,
    },
  },
})

const invocation = parser.parse(['run', '-f', 'worker.ts', '--', '--inspect'])
// invocation.options.file === 'worker.ts'
// invocation.inputs === ['--inspect']

Keep -- in documented examples. It makes the boundary between wrapper options and child arguments explicit and avoids changing existing calls when the child adds an option.

Capability boundaries

func/parser intentionally solves only the conversion from argv to typed data. The application must implement the following needs or move to full func:

Needfunc/parser boundary
Command structureOne command level; no nested paths, command aliases, handlers, or fallback.
Positional inputsA string[] only; no names, counts, types, or required rules.
Defaults/validationFixed empty values; no custom defaults, required, enum, dependency, or checks.
Help/discoveryNo descriptions, generated help, version, deprecation, or shell completion.
ExecutionNo dispatch, managed IO, exit codes, cancellation, or process error handling.
OrganizationNo modules, dependency injection, provider visibility, or lifecycle hooks.
TestingDirect parse(argv) tests only; no provider overrides, IO capture, or outcome.

The parser rejects unknown options and has no allowUnknown mode. Use passthrough on the relevant command and define exactly where downstream inputs begin.

When to migrate to func

A full application model usually becomes simpler when any of these signals appear:

  • Commands or actions need more than one level, or result-narrowing branches keep multiplying.
  • Manual help, option definitions, and tests have already drifted apart.
  • Commands share configuration, clients, caches, or resources that require cleanup.
  • The interface needs required values, enums, cross-field constraints, defaults, or clearer positional semantics.
  • The application needs consistent IO, errors, exit codes, cancellation, logging, configuration, or completion.
  • Tests need to execute a complete command while replacing providers and capturing stdout / stderr.

Migration does not require rewriting business logic. Move the command layer to its matching func construct while leaving the business functions intact:

func/parserfunc
commands.build@Command('build')
Boolean@Flag()
String / Number@Value() on a string / number property
{ multiple: true, type: String / Number }@ArrayString() / @ArrayNumber()
inputsargs.inputs from @Args()
if (command === ...)@Handler() or a path handler
Root optionsA root @Option(); action options use @OptionCommand() or features
Manual try/catch and cleanupCommand/module onError, onInit, and onDispose

This is an equivalent starting point for the earlier build command. The business build() function stays unchanged, while func owns command selection, field binding, and the help action:

src/index.ts
import { Args, ArrayString, Command, Handler, Module, Value, createApp } from 'func'
import type { Args as CommandArgs } from 'func'
import { HelpOption } from 'func/help'

@Command({
  path: 'build',
  description: 'Build one or more entries',
})
class BuildCommand {
  @Value({ alias: 'o', name: 'out-dir', description: 'Output directory' })
  outDir?: string

  @ArrayString({ alias: 't', name: 'tag', description: 'Release tag' })
  tags: string[] = []

  @Handler()
  run(@Args() args: CommandArgs) {
    return build({
      entries: args.inputs,
      outDir: this.outDir,
      tags: this.tags,
    })
  }
}

@Module({
  commands: [BuildCommand],
  options: [HelpOption],
})
class AppModule {}

const app = createApp(AppModule, { appName: 'tool' })
void app.bootstrap()

See Quick Start for the full project structure, then Commands and Field Options for modeling details. If you are still choosing surrounding tools, continue to the Ecosystem Guide.