-
Notifications
You must be signed in to change notification settings - Fork 7
Description
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 floatboolean
:true
orfalse
- 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
and0.foo
are equivalent
- e.g.
- 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"
orfoo["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
(wheretype
isnumber
,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 thestd.Recipe
type)