Skip to content

Support passing function arguments with brioche build #344

@kylewlacy

Description

@kylewlacy

Running brioche build calls the exported TypeScript build function (usually the default export) without any arguments. Today, we have some packages with build functions that optionally take arguments too:

We should allow brioche build to optionally call functions like these with extra arguments.

...actually, this turns out to be kinda hard to do properly! Build functions can take arbitrary JS values as arguments of course-- although it's a common convention that we pass a single object with different options as keys. So we need a way to decide how to map from CLI arguments to JS value arguments. Using TypeScript does give us more options, since we can determine the types of arguments a function expects.

My idea is to accept the flag --arg (-a), which takes a string of the form key=value. value will be parsed into a primitive JS value depending on the TypeScript type we infer:

  • string: Passed as a string value directly (no quoting or escaping)
  • number: Parsed as a float
  • boolean: true or false
  • Other types are not allowed

key acts like a dot-separated path to set, with a few rules:

  • If the first component is a number, it acts as a 0-based index of the arg to set. Otherwise, it applies to the first arg.
    • e.g. foo and 0.foo are equivalent
  • If the component is a number surrounded in square brackets, it's an array index
  • Otherwise, treat it as an object property name (must match /[a-zA-Z_][a-zA-Z0-9_]*/)

Examples

interface Options {
  foo?: string,
  bar?: number,
  baz?: boolean,
  inner?: {
    items?: string[],
  },
}

export default function example(options: Options = {}, extra?: number) { }
  • brioche build
    • example()
  • brioche build -a 'foo=hello'
    • example({ foo: "hello" })
  • brioche build -a 'bar=123'
    • example({ bar: 123 })
  • brioche build -a 'baz=true'
    • example({ baz: true })
  • brioche build -a 'inner.items[0]=a' -a 'inner.items[1]=b'
    • example({ inner: { items: ["a", "b"] } })
  • brioche build -a '0.bar=123' -a '1=456'
    • example({ bar: 123 }, 456)

Other thoughts

There's definitely a lot of edge cases around TypeScript types! I'm mainly worried about the happy path, but a few ideas for things we could run into:

  • For unions, I think we use prefer strings, then numbers, then booleans (or otherwise whatever's easiest to implement)
  • For generics, I'm fine with disallowing them entirely for an MVP
  • For overloads, we could do something simple like exclusively use the first overload for MVP

Also, performance could be a challenge, since we'll need to parse type information-- which implies loading the TypeScript compiler. For a first pass, I'm fine with adding extra restrictions if needed, such as only loading the project root for type-checking (and failing if there's not enough info to determine an argument type). Or as an extreme measure, we could look at the TypeScript AST directly and use that to try and determine the types ourselves (this would likely be very simplistic so would only support the most simple type declarations). I'd rather commit to something fast than back ourselves into a corner with something slow.

We could also allow passing more exotic things, likely as a follow-up:

  • We could allow passing objects or arrays wholesale using JSON syntax. We could do that by being smart about the --arg flag, or use a separate --arg-json flag to be explicit. Or maybe a single --args-json to pass all args as an array directly?
  • We could allow more object property names using double quotes (not sure if it should be foo."bar-baz" or foo["bar-baz"]). This will make parsing the argument strings more complicated of course, since that means we could no longer just split on =!
  • We could allow explicit type annotations using the syntax key:type=value (where type is number, string, etc. to disambiguate, similar to CMake)
    • If inferring argument types proves to be too difficult, we may even want to require this initially?
  • We might even be able to pass artifacts as arguments, by turning a filesystem path into an artifact and passing it in? (This may not be feasible or may be really difficult to do, since std is fully userland, and the JS runtime has no notion of the std.Recipe type)

Metadata

Metadata

Assignees

No one assigned

    Labels

    cliRelated to the Brioche command line interface

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions