nathanjhood / ts-esbuild-react

A React starter project template, powered by esbuild's Typescript support, with hot-reloading dev server.
https://nh-pages.stoneydsp.com/ts-esbuild-react/
Other
0 stars 0 forks source link

WIP/Development/server #11

Closed nathanjhood closed 4 weeks ago

nathanjhood commented 1 month ago

On the main branch, we've got a simple React project - written in Typescript/TSX - being bundled, served, and watched (with hot reload/fast refresh) by ESBuild, instead of the usual Webpack/Babel used by react-scripts.

I have no issue with either Babel or Webpack, but I quite like ESBuild for numerous reasons, not really worth going into too deeply here.

Speed is only one factor. To be fair, if speed were the only concern, Vite is the way to go.

I like being a little closer to the raw materials - partly, because I intend to diverge from what most front-end developers are doing with these tools. Not greatly, but just enough that I need a little bit more control over the raw materials; loaders, plugin configs, module aliases, and so forth (think react-native-web, think native modules...).

What I have on main is a solid working concept of what I want esbuild to do for me - the package.json script commands offer a similar set of functionality to react-scripts's commands, but using esbuild instead of babel/webpack. It isn't as clean, there are no 'modules' for each command, the commands themselves don't do a great deal of error handling, leave messy globals everywhere, there are various bits and pieces that could be given a nice polish and be packaged up to run something a bit closer to an executable...

And so the target for a new experiment becomes clear: let's have a go at converting the working concept into a command line executable; we could then bundle the project and deploy it over npm, add it to our React/NextJS projects as a dependency, and work with it as a driver.

Why bother?

I've got an equal amount of time and curiosity on my hands.

Besides that, I quite possibly will want to use it for the exact reasons highlighted in my motivations, above. I am getting quite close now to having the toolchain and the skillset I need in order to create the kinds of things that I am planning to create, and this one potentially represents one of the biggest links in the chain.

vercel[bot] commented 1 month ago

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
ts-esbuild-react ❌ Failed (Inspect) Aug 17, 2024 3:57am
nathanjhood commented 1 month ago

On this branch, I've ported the CLI tools from nathanjhood/base64 (credits in that repo) to a Typescript class - it has to be a class, since I could not get static consts to exist globally, statics must be outside a module or namespace! - and have a running executable out of it.

ESBuild is not yet connected to this CLI; I've simply written the parseArgs() - roughly - and a public process() method, and currently it successfully globs files and known flags and does some rough validation with it.

I'll probably attempt to pass the globbed file results into entry points for esbuild, and offer a flag for each mode, such as --dev and --build ... the actual functionality of which I already have proven, on main, but will need to clean up a bit to get these parts orchestrating together.

side note: I'm currently executing the typescript directly with TS-node, but the final intention would probably be to transpile into JS and ship that, like a regular person would :) , mostly for performance reasons.

nathanjhood commented 1 month ago

Here, in the createCli() function, I've just hard-coded an arg into the array, in this case the esbuild.config.ts file:

const createCli = async () => {
  const cli = spawn(
    './index.ts',
    ['esbuild.config.ts'],
    {
      shell: true,
      signal: signal,
      env: process.env
    }
  );

note: './index.ts', can be replaced with whatever the final executable module shall be

Instead, the idea is that we should pass process.argv:

const createCli = async () => {
  const cli = spawn(
    './index.ts',
   process.argv,
    {
      shell: true,
      signal: signal,
      env: process.env
    }
  );

The tricky bit is that we need to increment argv first, to avoid passing the program name to the file glob pattern. Not tricky per sé, but it may be worth considering whether to parseArgs a bit eariler, or do some kind of validation within the frame of this function call.

Let's try something.

nathanjhood commented 1 month ago

Now, we need to add some entries to the CLI class list of accepted args:

// flags
      if (!(this._input_args.length === 0)) {
        if (arg === /--version/.source || arg === /-v/.source) {
          if (this._show_version) {
            const msg = "cannot use -v/--version parameter twice!";
            throw new Error(msg);
          }
          this._show_version = true;
          break;
        }
        if (arg === /--help/.source || arg === /-h/.source) {
          if (this._show_help) {
            const msg = "cannot use -h/--help parameter twice!";
            throw new Error(msg);
          }
          this._show_help = true;
          break;
        }
        if (arg === /--showConfig/.source) {
          if (this._show_help) {
            const msg = "cannot use --showConfig parameter twice!";
            throw new Error(msg);
          }
          this._show_help = true;
          break;
        }

For example:

        if (arg === /--build/.source || arg === /-b/.source) {
          if (this._run_build) {
            const msg = "cannot use --build/-b parameter twice!";
            throw new Error(msg);
          }
          this._run_build = true;
          break;
        }

Later, in process() or processArgs(), we'll use this set of flags to choose a particular script from the ones implememented in the example project, as shown currently on main.

At the moment, the validation doesn't quite work - doubling of args doesn't throw an error as expected (or, the error is not caught?). So, I'm concerned that we'll be able to both --build and --dev in one pass on the CLI at the moment, which is not desirable behaviour. Instead, I'd like to throw an error in these cases to tell the user how to use the CLI args as intended.

So I think that the validation, and probably some tests of, is blocking us before we can start calling the dev/build/analyze scripts.

Otherwise, we aren't so far off.

Another command might be --init, which could re-write the example project in-place... one of the templates somewhere in react native world (maybe it was expo) has a script along those lines we can use as a starter. It's a volatile idea though, of course.

nathanjhood commented 1 month ago

So TS-node is a bit tricky to work with and seems to add a layer of brittle-ness to the project currently, which is distracting me from my actual goals - I only want to develop this project with Typescript, but plan to deploy it as bundled Javascript.

It turns out my needs are well met by tsx - i.e., it's just working straight out of the box, and hasn't thrown any errors so far. Not to mention, it's almost as fast as node itself, at least on these very small scripts.

So I'll try to minimize the side-quest about Typescript runtime tooling by migrating from TS-Node to tsx.

The aim here is to essentially write the Typescript as close as possible to what the optimized JS output would look like; ideally, building for deployment should be as clean as just removing the typings (moving them into JSDoc comments perhaps). Getting this right means avoiding certain operators and constructs which only exist in Typescript, but without minimizing the type safety and error checking/validation I'd use those constructs for.

To achieve any of that, I need a nice smooth runtime. TSX appears to be seamless. I'll do a bit more playing around with it before committing the migration, though.

nathanjhood commented 1 month ago

tsx and pkgroll for the win.

In fact I almost wonder if these tools can do some of the tasks as good/better than esbuild... certainly they have provided a lot of clarity on the project config required to pull this off.

nathanjhood commented 4 weeks ago

So this got a bit superceded - I ended up just borrowing the contents of react-scripts and swapping out the Webpack bits for esbuild's equivalent functionality.

Along the way, I figured out what the "eject" command is all about and how it works. I've got an entrypoint on it to work with now.

There is some good work on this PR, such as porting the C++ CLI to typescript. That is a pretty cool project in itself, though now I've finally gotten around to working with the NodeJS libraries Commander and Chalk, it does seem like a timesink for little reward.

I'll probably park this PR and branch for a minute while I continue doing a direct port of react-scripts using esbuild. I can revisit it later and skim some of the ideas, if still relevant.