-
Notifications
You must be signed in to change notification settings - Fork 36
feat(cli): create cli package with config, plugins and commands #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left some questions
| "src/**/*.test.ts", | ||
| "src/**/*.spec.ts", | ||
| "src/**/*.d.ts" | ||
| , "__tests__/getConfig.spec.ts" ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Use pattern here.
- Do we want to use top-level
__tests__mirroring thesrcfolder structure or put__tests__folder in thesrctree, next to the actual code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was autogenerated by Nx I guess. I think it makes more sense to put __tests__ in src
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to __tests__ side by side the source, alternatively x.test.ts convention is also nice
packages/cli/package.json
Outdated
| @@ -0,0 +1,18 @@ | |||
| { | |||
| "name": "@rnef/cli", | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] @callstack/rnef-cli?
| * } | ||
| * ); | ||
| */ | ||
| export const writeFiles = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General questions, should we use sync io or rather opt for async/promise io to be able to display some fancy spinners (you need async io for that, right?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sync file access is generally faster in test environments so I prefer that
| .version(version); | ||
|
|
||
| export const cli = async () => { | ||
| const config = await getConfig(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we plan to handle case when two plugins define the same command? Last one wins or some error/warning to the user?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initially I thought that for now it's ok that last one wins and getConfig eventually warns about 2 duplicate command names. I was also thinking about overriding mechanism like this:
module.exports = {
plugins: {
pluginAndroid,
pluginApple,
android2: otherAndroidPlugin, // <-- when defined this way we could prefix commands with `android2:`
commands: { // <-- if we keep the commands in user config then this could also be possible
[pluginAndroid.commands.build]: CustomCommand
}
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we last discussed the pom configuration mechanism, you expressed some concerns about non-deterministic nature of arrays. In other words, the result being dependent on the order configs were loaded. How does that concern apply to this?
(I generally think it is hard to solve)
packages/cli/src/lib/getConfig.ts
Outdated
| type ConfigType = { | ||
| projectConfig?: object; | ||
| plugins?: Record<string, PluginType>; | ||
| commands?: Array<{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] extract CommandType
packages/cli/src/lib/getConfig.ts
Outdated
| commands?: Array<{ | ||
| name: string; | ||
| description: string; | ||
| action: (config: ConfigType) => void; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit^2]
| action: (config: ConfigType) => void; | |
| run: (config: ConfigType) => void; |
|
|
||
| const extensions = ['.js', '.ts', '.mjs']; | ||
|
|
||
| const importUp = async <T>(dir: string, name: string): Promise<T> => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this looks up to find the first rnef.config? Do we want to support multi-level ones? Monorepo, etc?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would assume you have a single rnef.config sitting in your monorepo root, or where RN is located. Since it's a JS file you could import project-specific config from other locations, if that's what you're after
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is interesting question and I initially had a different approach (sitting at the project level, stop when you hit first one) and I don't think that both are correct.
The problem here is when you have multiple React Native applications, or you want to customize behavior on per package basis. I think eventually we may need both.
For example, you have one application where you want to load different set of plugins (e.g. for brownfield), and the other one, being your greenfield sandbox, you want different set.
Again, this is likely an edge case scenario, but the idea was to make all the options possible w/o the CLI design being the limitation here.
packages/cli/src/lib/getConfig.ts
Outdated
| type ConfigType = { | ||
| projectConfig?: object; | ||
| plugins?: Record<string, PluginType>; | ||
| commands?: Array<{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
q: Do we expect user to define their own commands in the config?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Up to us. For now I allow for this, but making a plugin is so simple, to we could also only allow making commands through plugins to simplify our config handling
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for requiring use to make a simple plugin
packages/cli/src/lib/getConfig.ts
Outdated
| }; | ||
|
|
||
| type ConfigType = { | ||
| projectConfig?: object; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
empty placeholder for now. I was thinking we could put some generic platform-independent project config here, like root of the project, or RN path, but it doesn't have to be here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] add code comment to descibe this intent
packages/cli/src/lib/cli.ts
Outdated
| .description(command.description || '') | ||
| .action(async () => { | ||
| try { | ||
| await command.action(config); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we pass here the full config?
I guess the most useful for a plugin would be to get the config.projectConfig because the rest is just plugins/commands?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should definitely strip the config to essentials. What I was also considering is adding some helpers like config.api.registerCommand instead of returning the commands as an output, although I picked to latter for now due to simplicity
packages/cli/package.json
Outdated
| "tslib": "^2.3.0" | ||
| }, | ||
| "devDependencies": { | ||
| "jest-util": "^29.0.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is more of a general question, but why do we use jest over something such as vitest? Works better with TypeScript, ESM (for the future), and has a nice workspace-mode that allows running these faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with vitest, ship it. It was the default from Nx I guess, would need @mdjastrzebski to confirm. I'm also good with ESM, we should decide wether we roll with it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's the nx default and we just rolled with it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feel free to tinker with setting up ESM, vitest, etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR! Being one of the early PRs, you have this opportunity of getting extra comments, not always related to the PR itself!
Anyway, I left some comments regarding the CLI design and architecture. I don't think we discussed this, so I would like to get y'all opinion on the comments.
| "bin": { | ||
| "rnef": "./src/bin.js" | ||
| }, | ||
| "type": "commonjs", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Isn't this the default?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nx template I guess
| "main": "./src/index.js", | ||
| "typings": "./src/index.d.ts", | ||
| "private": true | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This can leave as a separate TBD, but I think, if we plan to eventually publish each separate module to npm, we should take care of adding all necessary fields to the package.json, such as: description, author, license, repository.
|
|
||
| describe('cli', () => { | ||
| it('should throw when config not found', async () => { | ||
| await expect(cli()).rejects.toThrow('rnef.config not found in any parent directory of /'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Do we expect the config to be always defined, a/k/a no defaults? Why?
| .version(version); | ||
|
|
||
| export const cli = async () => { | ||
| const config = await getConfig(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When we last discussed the pom configuration mechanism, you expressed some concerns about non-deterministic nature of arrays. In other words, the result being dependent on the order configs were loaded. How does that concern apply to this?
(I generally think it is hard to solve)
| const config = await getConfig(); | ||
|
|
||
| // Register commands from the config | ||
| config.commands?.forEach((command) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: One of the design decisions I made in pom was to avoid hardcoding things on the config that do not relate to the configuration of the CLI itself (e.g. plugins, runtime etc.). That said, I always felt like commands were more related to a CLI runtime (in this case, on top of Commander), than to the CLI itself.
In the current design, commands will be loaded ahead of time when you call getConfig() which I guess is fine and somewhat similar to what we've been doing in the CLI, however, the other approach had the benefit of being lazy and more modular, leaving it up to the "commander plugin" itself to define shape of Commands it expects.
I think this is good opportunity to talk about this. I briefly touch-based on it on Slack, but I think we can move that discussion over to here.
packages/cli/src/lib/getConfig.ts
Outdated
| ]; | ||
| } | ||
|
|
||
| delete config.plugins; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: is there some sort of performance benefit to reusing config and deleting plugins rather than just returning the "final shape" directly from the scratch? I am worried about config having some extraneous properties later, once users start adding new properties on top.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for creating final config as a new object
| "noPropertyAccessFromIndexSignature": true, | ||
| "noImplicitReturns": true, | ||
| "noFallthroughCasesInSwitch": true, | ||
| "resolveJsonModule": true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: if we extend the base, shouldn't we put all these into the base already? Some of these don't seem like CLI specific, such as strict: true. I guess we should make sure we have as shared config as possible for consistency.
| "src/**/*.test.ts", | ||
| "src/**/*.spec.ts", | ||
| "src/**/*.d.ts" | ||
| , "__tests__/getConfig.spec.ts" ] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to __tests__ side by side the source, alternatively x.test.ts convention is also nice
| @@ -0,0 +1,14 @@ | |||
| { | |||
| "extends": "./tsconfig.json", | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This is more of a question related to Nx, than this PR. What is the purpose of both lib and spec configs, as well as base?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sa far as I undestand it:
tsconfig.lib.jsonis build configtsconfig.spec.jsonis for testing- and
tsconfig.jsonis the base one (for the projects, as there is alsotsconfig.base.jsonat the root)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should discuss whether we want to go with Nx defaults (probably would be easier for us), or to deliberately set the things up in the way want, but to make sure we do it consistently between the projects
| "module": "esnext", | ||
| "lib": ["es2020", "dom"], | ||
| "skipLibCheck": true, | ||
| "skipDefaultLibCheck": true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: Not related to this PR, but I missed the Nx setup (CC: @mdjastrzebski). I usually prefer using one of @tsconfig by Microsoft that is aligned with the Node version I am using. That way, these do not live hardcoded and are easier to roll as we upgrade the engine requirements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just rolled with Nx defaults, feel free to submit issue / PR with changes.
affa68b to
584c63c
Compare
584c63c to
8c067f0
Compare
|
Superseded by #15 |
Summary
First draft of a very simple config system that loads plugins defined as:
and registers the commands using
commanderfor now. In the future I'd like to use a lighter tool, but let's start with something simple.Test plan
Added unit tests with some helpers that I'll need to move to a separate package (done in #7) I suppose to not validate module boundaries