gajus / turbowatch

Extremely fast file change detector and task orchestrator for Node.js.
Other
943 stars 23 forks source link

A list of a few small things I encountered as a new user (and your newest fan) #41

Closed DesignByOnyx closed 1 year ago

DesignByOnyx commented 1 year ago

Desired Behavior

I am a new user and am now a huge fan of this tool (I'm a long time nodemon user)! Thanks so much! I started an issue for one thing, but as I've been using the tool there are several other things... so I thought I'd list them in one place for triage. Please let me know if you'd like me to break this into smaller pieces.

Motivation

I just want to make this tool cool as ever. These are merely suggestions, coming from a long-time nodemon user and a turborepo user who desperately wants a watch script.

gajus commented 1 year ago

You should support all node LTS versions. I know 14 and 16 will be dead later this year, but if you install turbowatch on 14 or 16 today it says that that version is not supported. This isn't a very good first taste for new users. FWIW - I'm on node 16.19.0 and it seems to be working just fine!

See the point about the start time below.

Export ProcessPromise, et al. I am needing to save a reference to one of the processes that I spawn, and I need the types.

Generally, I am careful about not exporting anything that is not a public interface. ProcessPromise is an oversight though. Anything else that you were missing?

Add an onStart callback, or explain that onChange is called during startup with an empty files array.

That's what the initialRun is for. The benefit of the current approach VS adding onStart is that you can re-use the logic in your onChange callback between initial run and future change events.

However, I am open to considering getting rid of initialRun in place of onStart, as the latter is more explicit. Will open a separate issue to track it.

Show some use cases for using multiple triggers. I don't understand the need for an array.

Example: In the same project, we have a steps to...

I am open to adding examples, but it is effectively any time when you need several different workflows.

Using project: __dirname is very slow on a small monorepo, even if I have an expression to ignore node_modules. It takes roughly ~6s to start. If I watch a single folder, it takes ~40ms. I usually only need to watch 4-5 folders at a time. It would be nice if I could pass an array of these folders to the project instead of passing the repo root __dirname.

Regarding start time, this is because of your Node.js version. Node.js v19 and upwards supports fs.watch recursively. Earlier versions require scanning the entire file tree, which is going to be very slow. Therefore, realistically, this project will only have desirable DX in Node.js v19 and upwards.

You can of course bring your own backend. However, that most likely will require installing a third-party software like Watchman, so it isn't the default.

Coming from nodemon, maybe consider a simplistic API for easy use cases and easy adoption, keeping the existing [more verbose] API for more advanced scenarios. The expression syntax is kinda weird.

I am open to exploring it (in a separate issues), but generally speaking it is not a simple problem b/c of BYO backend architecture. The most simple abstraction is "watch all, and use Turbowatch to filter relevant results". The moment you lift some of this logic up to the backends, then you make some of them incompatible with Turbowatch, e.g. Watchman very much relies on a single root + filter logic, and it is by far the most cross-platform compatible backend.

Thanks for your suggestions. Will open separate threads for some of these suggestions to track them.

DesignByOnyx commented 1 year ago

Thanks for all of your feedback (and sorry to dump it in one big message). Everything you said makes perfect sense.

type SimpleConfig = {
    script?: string;
    ext?: string;
    exec?: string;
    delay?: number;
    watch?: string | string[];
    ignore?: string | string[];
}

const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

function watch(config: SimpleConfig | TurbowatchConfigurationInput) {
    if (!('triggers' in config)) {
        // using simple syntax
        const { script, ext, exec, delay, watch: directories, ignore } = config;
        const extensions = ext || 'js,mjs,coffee,litcoffee,json';
        const expression: Expression = ['allof'];
        const waitMS = delay || 1000;

        expression.push(['anyof', ...extensions.split(',').map(e => ['match', `*.${e}`, 'basename'])])

        if (directories) {
            const dirArr = typeof directories === 'string' ? [directories] : directories;
            expression.push(['anyof', ...dirArr.map(w => ['directory', w])])
        }

        if (ignore) {
            const ignoreArr = typeof ignore === 'string' ? [ignore] : ignore;
            expression.push(['not', ['anyof', ...ignoreArr.map(i => ['match', i, 'wholename'])]])
        }

        return watch({
            project: process.cwd(),
            triggers: [{
                name: 'simple-watch-task',
                initialRun: true,
                interruptible: true,
                expression,
                onChange: async ({ spawn, first, abortSignal }) => {
                    if (!first) await sleep(waitMS);
                    if (abortSignal.aborted) return;
                    if (script) await spawn`node ${script}`;
                    else if (exec) await spawn(exec);
                }
            }]
        });
    }
    // existing code here
}