Skip to content
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

[RRFC] Pluggable script runners #691

Open
justinfagnani opened this issue Apr 18, 2023 · 5 comments
Open

[RRFC] Pluggable script runners #691

justinfagnani opened this issue Apr 18, 2023 · 5 comments

Comments

@justinfagnani
Copy link

justinfagnani commented Apr 18, 2023

Motivation ("The Why")

There are a number of tools, and a few RRFCs issues here (#190, #610 and #548) that relate to running scripts in series, parallel, etc.

The existing RRFCs are in some ways relatively simple, but the area of how and when to run scripts is quite deep and complex, and beyond what order to run scripts in, quickly gets into things like:

  • Dependency graphs of scripts
  • Freshness-checking of script inputs
  • Caching script outputs
  • Displaying script console output
  • Handling long-running ("service") scripts: dependencies between them, noticing when ready, when to restart

It seems like npm has a few options on how to proceed with more sophisticated script running, ranging from:

  1. Do nothing and leave it up to external tools
  2. Incrementally add more features, like serial and parallel scripts, and grow in complexity
  3. Design and implement a full-featured script graph system akin to Wireit

While I would love to see a great script runner built-in, I think all three of these options have serious downsides.

I'd like to propose an alternative that should be simple and allow for external project to extend npm script running capabilities, possibly creating well-worn paths for npm to adopt later, which is pluggbale script runners.

A script runner would be registered in a package.json file and get to intercede on running all scripts.

Ex:

{
  "runner": "wireit",
  "scripts": {
    "build": "tsc",
    "start": "web-dev-server"
  },
  "devDependencies": {
    "wireit": "^0.9.5"
  }
}

Upon npm commands that invoke scripts, like npm start, npm test, npm run, etc., npm would call the runner with the script to run and command line flags.

Alone, this doesn't enable much (though a runner could have flags run scripts of monorepo dependencies, etc), so we need a way to add more metadata about scripts for runners to consume. Letting scripts be objects instead of just strings would solve this.

{
  "runner": "wireit",
  "scripts": {
    "build": {
      "command": "tsc",
      "inputs": ["src/**/*.ts"],
      "outputs": ["build/"]
    },
    "start": {
      "command": "web-dev-server",
      "dependencies": ["build"]
    }
  },
  "devDependencies": {
    "wireit": "^0.9.5",
  }
}

With this additional information the script runner can now check inputs for changes, cache build outputs, and run dependencies or check that they're fresh.

Certain script metadata fields can be standardized, most importantly "command" which would be supported by the built-in runner. Other fields would be metadata for specific runners.

Over time the "runner" field could be a place to opt-in to new built-in running behavior.

Note that this is essentially building in more ergonomic support for what's possible today if you delegate all script to a runner manually. Wireit works on package.json configs like this:

{
  "scripts": {
    "build": "wireit"
  },
  "wireit": {
    "build": {
      "command": "tsc"
    }
  }
}

I think explicitly carving out space for runners would discourage some of the fragmentation in the ecosystem where many runners have their own CLI that side-steps npm and creates a new thing to learn and barrier to entry for new contributors. The script runner approach keeps things fairly well-hidden behind plain npm commands.

Example

See above.

How

Current Behaviour

Currently scripts only execute the command given, and don't run in series, parallel, or with dependencies. Custom script runners have to be manually setup with their own script definition sections in package.json.

Desired Behaviour

Allow runners to bring the features of tools like Turborepo, Nx, Wireit, etc, but keep the external interface of the npm CLI.

References

Alternatives

An alternative would be to build the features of, say Wireit, directly into npm. I actually think this would be a great outcome, especially for workspaces, but I've been assuming that it's a bit to big of an ask.

@justinfagnani
Copy link
Author

cc @aomarks who created Wireit

@bmeck
Copy link

bmeck commented Apr 20, 2023

I'm incredibly hesitant around this given various feedback we are seeing around our socket npm wrapper. I think adding features to allow for parallelism is fine, but various problems exist unless full locking etc. is done by the downsteam tools. You need to lock the STDIO for prompts/spinner coordination properly (or face lack of ability to do various features), lock files to define collisions on disk, etc.

I think working on that locking is at least our main struggle in this area as we are exploring things in a way that doesn't break existing workflows. Running with pluggable script runners means both opening up complexity for reproducibility and for still needing to understand failure cases of these tools.

Certainly would like some kind of pluggable system, but we have likely greater needs for our work than just scripts.

@justinfagnani
Copy link
Author

I don't think this introduces any new capabilities. You can already write standard npm scripts that delegate to a script runner, which is how Wireit works today. This proposal would make that more ergonomic to configure by package maintainers.

I do agree that script runners need to carefully consider locking, etc., but this is already true today.

@trusktr
Copy link

trusktr commented Oct 14, 2023

Script coordination and locking does not need to be a feature of NPM in order to ship a feature like wireit that makes script ordering super simple to manage.

Lerna and Wireit have both extensively proven this, and are extremely useful.

Now that I've used Lerna (and intend to switch to Wireit) I can't imagine, for example, wanting to switch from either of those (with topological script ordering) to manually telling NPM the order of scripts and have to keep updating that when I refactor scripts. That would be a really huge pain in comparison to using Lerna or Wireit.

Most of the time, people using this hypothetical feature will be managing a multi-project repo (monorepo, or repo git submodules) and if they have conflicts between their scripts they can solve that problem on their end, without resource locking being a requirement that blocks shipping something incredibly useful for NPM users.

@bhouston
Copy link

I tried out wireit @justinfagnani and I found that it didn't handle topological sort automatically. It wanted me to list out dependences between different pages in the monorepository manually when it was technically already in the package.json file.

Instead of wireit, I adopted lerna/nx and it works great. My template project that makes use of lerna/nx is here: https://github.com/bhouston/template-typescript-monorepo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants