AlexPshul / nxazure

MIT License
16 stars 7 forks source link

Watch/HMR #15

Closed JonSilver closed 1 year ago

JonSilver commented 1 year ago

Thanks for creating this nx generator - it's much needed.

I'm developing a library for use with Azure Function Apps. Is it possible to have a library under development watched, hot rebuilt, and the function app hot reloaded whenever it or its dependent library changes? I've tried many things, but nothing worked. It seems like this highly necessary functionality might be an impossibility.

AlexPshul commented 1 year ago

This is definitely on my to-do list to figure out how to do. Especially in a microservices environment, this is a very much needed feature. I'll ping you back here once I future this out.

JonSilver commented 1 year ago

Thanks @AlexPshul. I played around some more, and came up with a VSCode tasks.json solution that works pretty well.

Given an nx monorepo with the following projects:

... and the following task breakdown:

The whole thing can then be started up with just a Ctrl-Shift-B and everything will hot reload... ish. If you modify the library or the API you'll still have to refresh the demo app in the browser.

So the tasks.json looks something like this:

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "Build Lib",
            "detail": "Build Lib",
            "type": "shell",
            "command": "nx build api-library && npx -c 'nx watch --projects=api-library -- nx run api-library:build:development'",
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "isBackground": true,
            "problemMatcher": {
                "fileLocation": "relative",
                "pattern": {
                    "regexp": "error",
                    "file": 1,
                    "location": 2,
                    "severity": 3,
                    "code": 4,
                    "message": 5
                },
                "background": {
                    "activeOnStart": true,
                    "beginsPattern": "Compiling TypeScript|nx run",
                    "endsPattern": "Successfully"
                }
            },
            "group": {
                "kind": "build"
            }
        },
        {
            "label": "Build API",
            "detail": "Build API",
            "type": "typescript",
            "tsconfig": "./packages/api/tsconfig.build.json",
            "option": "watch",
            "options": {
                "cwd": "${workspaceFolder}/packages/api"
            },
            "isBackground": true,
            "dependsOn": ["Build Lib"],
            "problemMatcher": "$tsc-watch",
            "group": {
                "kind": "build"
            }
        },
        {
            "label": "Start Backend",
            "detail": "Start API",
            "type": "shell",
            "command": "nx start api",
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "dependsOn": ["Build API"],
            "isBackground": true,
            "problemMatcher": "$func-node-watch",
            "group": {
                "kind": "build"
            }
        },
        {
            "label": "Start Frontend",
            "detail": "Start UI",
            "type": "shell",
            "command": "nx serve demo",
            "options": {
                "cwd": "${workspaceFolder}"
            },
            "dependsOn": ["Start Backend"],
            "isBackground": true,
            "problemMatcher": {
                "owner": "typescript",
                "fileLocation": "relative",
                "pattern": {
                    "regexp": "error",
                    "file": 1,
                    "location": 2,
                    "severity": 3,
                    "code": 4,
                    "message": 5
                },
                "background": {
                    "activeOnStart": true,
                    "beginsPattern": "nx run demo",
                    "endsPattern": "Local:"
                }
            },
            "group": {
                "kind": "build",
                "isDefault": true
            }
        }
    ]
}

The odd way of calling nx watch here is because I've never managed to get it working any other way in a Windows/Powershell environment.

It's not a perfect setup, but for the most part it achieves what I set out to find. If anyone can think of ways to improve it, that'd be great!

JonSilver commented 1 year ago

Ah... now it's started misbehaving when I edit the API source code... at the first API call afterwards, it loses connection with the library build files and gives this error:

Exception: Worker was unable to load function myEndpoint: 'Cannot find module '@mylib/api-library'

AlexPshul commented 1 year ago

Thanks @JonSilver for the workaround. The reason it doesn't work for you in the way you described is because there is a code that being injected in the final build files to support libraries recognition from the paths property in tsconfig file.

You could work around the issue (until I work a proper solution) by importing in your code the _importPaths.ts file before importing any other lib. This is basically what I automatically inject in the build process.

Let me know if it works for you.

JonSilver commented 1 year ago

Where's that file, @AlexPshul? I can't seem to find it in my workspace.

AlexPshul commented 1 year ago

Sorry @JonSilver, my bad. I meant the _registerPaths.ts file. It should be in the root directory of your function app.

JonSilver commented 1 year ago

@AlexPshul I'm so sorry, I'm probably being really thick, but how do I need to import my library?

My _registerPaths.ts looks like this:

import { register } from "tsconfig-paths";
import * as tsConfig from "../../tsconfig.base.json";
import { CompilerOptions } from "typescript";

const compilerOptions = tsConfig.compilerOptions as unknown as CompilerOptions; // This is to avoid any problems with the typing system

if (compilerOptions.paths) {
    const newPaths: Record<string, string[]> = Object.entries(compilerOptions.paths).reduce(
        (newPathsObj, [pathKey, oldPaths]: [string, string[]]) => {
            newPathsObj[pathKey] = oldPaths.map(path => path.replace(/.ts$/, ".js"));
            return newPathsObj;
        },
        {} as Record<string, string[]>
    );

    register({
        baseUrl: "dist",
        paths: newPaths
    });
}

What do I need to insert and where?

AlexPshul commented 1 year ago

Great! That's the file! In your function app, for each function you create, just call import '../_registerPaths' as the first line in the file. (If you are using V4 functions, you might need to change the path to the one that will suit your correct relative path)

JonSilver commented 1 year ago

Thanks, @AlexPshul, that seems to work really well. I now have that most beautiful of things, a smooth dev experience across isomorphic libraries, function apps and frontends, where everything hot reloads whenever something changes. Sweet! 🥇

Shall I leave this open for tracking the eventual official solution?

AlexPshul commented 1 year ago

Yes, I'll mark it as closed once it's implemented. Thanks!

AlexPshul commented 1 year ago

@JonSilver have a look at V.1.0.14. The start executor is now watching for changes. Let me know what you think.

JonSilver commented 1 year ago

It works! Thank you @AlexPshul

However library resolution and HMR seem very fragile. I have one project where it's working fine, and another where it's not working at all. :(

AlexPshul commented 1 year ago

Hmmm that should not be the case. Maybe it has something to do with configuration? If you can reproduce this issue in a small repo on the side, I can have a look at it.

JonSilver commented 1 year ago

Hmmm that should not be the case. Maybe it has something to do with configuration? If you can reproduce this issue in a small repo on the side, I can have a look at it.

New issue... I found a bug! 😁