nxext / nx-extensions-ionic

Nx Extension for Ionic
MIT License
32 stars 8 forks source link

Angular: Add ionic live reload #8

Open KevinHetzelGFL opened 1 year ago

KevinHetzelGFL commented 1 year ago

Is your feature request related to a problem? Please describe. For ionic there are this commands available to run the app with live reload on android or ios:

$ ionic capacitor run ios -l --external $ ionic capacitor run android -l --external

I realy need it for my ionic app inside nxext Workspace. At the moment I always need to run "nx run [app-name]:sync:android" and "nx run [app-name]:open:android" after changes. It takes a lot of time

mbeckenbach commented 11 months ago

@KevinHetzelGFL this works for me: https://github.com/nxext/nx-extensions/issues/773

Just tested with a fresh NX version and fresh app using ios and android with capacitor 5 on a mac. Works for both ios and android.

KevinHetzelGFL commented 11 months ago

@mbeckenbach please tell me which command you use. If i execute ionic capacitor run android -l --external I get this error: [ERROR] Sorry! ionic capacitor run can only be run in an Ionic project directory.

mbeckenbach commented 11 months ago

@KevinHetzelGFL just dont run it directly using capacitor.

Here is how i do it step by step:

  1. Adjust the capacitor.config.ts, to point to your nx serve instance.
import { CapacitorConfig } from '@capacitor/cli';
import ip from 'ip';

const config: CapacitorConfig = {
  ...
  server: {
    androidScheme: 'https',
    url: `http://${ip.address()}:4200`, // If you run on another port, adjust it
    cleartext: true,
  },
};

export default config;
  1. Change your project.json at the Run Task to add -l and --external to the commands and allow remote access to the serve instance

SERVE Target: The port and host are important here.

 "serve": {
      "executor": "@angular-devkit/build-angular:dev-server",
      "options": {
        "port": 4200,
        "host": "0.0.0.0"
      },
      "configurations": {
        "production": {
          "browserTarget": "aticash:build:production"
        },
        "development": {
          "browserTarget": "aticash:build:development"
        }
      },
      "defaultConfiguration": "development"
    },

RUN Targets:

"run": {
      "executor": "@nxext/capacitor:cap",
      "options": {
        "cmd": "run"
      },
      "configurations": {
        "ios": {
          "cmd": "run ios -l --external"
        },
        "android": {
          "cmd": "run android -l --external"
        }
      }
    },
  1. start your local dev server in a terminal

nx serve yourapp

  1. from another terminal run:

nx run yourapp:run:android or nx run yourapp:run:ios

mbeckenbach commented 11 months ago

@KevinHetzelGFL Please note that i tested this on MacOS. I tried to get it running on windows. But windows s*cks as always.

mbeckenbach commented 11 months ago

@KevinHetzelGFL I am not sure if this capacitor config change affects the real device builds too. I guess so. So be careful before publishing the app to stores!!!

If this really works when deployed to real devices, it opens new options for development & testing. :-D

Btw: Works on windows with android too. I just had some issues with Android Studio...

andregreeff commented 11 months ago

just to clarify a few things here...

  1. Adjust the capacitor.config.ts, to point to your nx serve instance.
import { CapacitorConfig } from '@capacitor/cli';
import ip from 'ip';

const config: CapacitorConfig = {
  ...
  server: {
    androidScheme: 'https',
    url: `http://${ip.address()}:4200`, // If you run on another port, adjust it
    cleartext: true,
  },
};

export default config;

doing it this way will break anything other than a "live reload development run".. the capacitor.config.ts file in your project's web src directory is only used to generate the capacitor.config.json in your project's native app project directories, and is only updated again the next time you run a cap sync (I think it's part of update, but I never run copy and update separately).

to get around this, you will need to conditionally modify your CapacitorConfig's server object. in my projects, I do this as follows:

import { CapacitorConfig } from '@capacitor/cli';
import { address } from 'ip';

const config: CapacitorConfig = {
  ...
  server: {
    androidScheme: 'https',
  },
};

// this dynamic configuration allows the Capacitor app to source its UI from the Angular webserver,
// which will then serve the UI dynamically with live reloading for development purposes.
if (process.env['NX_CLI_SET'] === 'true' && process.env['NX_TASK_TARGET_TARGET'] === 'run') {
  // NOTE: virtual adapters from the likes of VMWare or VirtualBox do tend to mess with this a little.. YMMV.
  // furthermore, the "ip" package's "address" method fails loudly..
  let wirelessIp: string | undefined;
  try {
    wirelessIp = address('Wi-Fi');
  } catch (error) {}

  let ethernetIp: string | undefined;
  try {
    ethernetIp = address('Ethernet');
  } catch (error) {}

  const serverIp = wirelessIp || ethernetIp;
  if (typeof serverIp !== 'string') {
    throw Error('could not find a valid server IP address to use for Live Reload..');
  }

  const serverPort = process.env['PORT'] || '4200';
  config.server = {
    url: `http://${serverIp}:${serverPort}`,
    cleartext: true,
  };
  console.log('custom Capacitor config.server:', config.server);
  console.warn('be sure to start the Angular dev server as well, this step is not automated (yet)..');
}

export default config;

in this case, process.env['NX_CLI_SET'] will be true simply because we're running commands with Nx CLI. whereas process.env['NX_TASK_TARGET_TARGET'] will be the actual Nx command being run, in this case being triggered by nx run <app>:run.

for context, running nx run <app>:build will not activate the config tweaks and the "live reload" configuration will not be injected into your application's capacitor.config.json file, in turn not breaking your production builds or your distributable dev builds (for Android anyway, since iOS doesn't allow direct installs from ipa files).


secondly, the --livereload and --external CLI args are not part of the Capacitor CLI, they're part of the Ionic CLI, which includes a subcommand that wraps Capacitor.

so, ionic cap run android --livereload will cause the internal web server to start up with "live reload" enabled, whereas cap run android --livereload will simply build the app and launch it on Android, exactly the same as cap run android would.

at the same time ionic cap run --external will cause the internal Angular web server to bind itself to the 0.0.0.0 IP address, allowing it to "serve" HTTP requests from something other than localhost, whereas cap run (without ionic) will not launch the Angular web server.


in your application's project.json, I would recommend modifying your serve target to include the following changes to your development config (or even create a new config), similar to the following:

"serve": {
  "executor": "@angular-devkit/build-angular:dev-server",
  "configurations": {
    "production": {
      "browserTarget": "live:build:production"
    },
    "development": {
      "browserTarget": "live:build:development",
      "host": "0.0.0.0",
      "liveReload": true
    }
  },
  "defaultConfiguration": "development"
},

you can leave the run target as it was, adding --livereload and --external have no effect there.

but then to get the app running with "live reload", you need to:

  1. open terminal 1
  2. run nx run <app>:serve:development - this will continue to run until your kill the process.
  3. open terminal 2
  4. run nx run <app>:run - this process will end once the native app has been built and deployed to your device or emulator.

keep in mind that the app deployed to your device or emulator by this will always look at the configured server.url address when it starts up, so once the native app has been built and deployed, all you need to do to continue your "live reload development" process is launch your Angular web server with live reload, and open your already deployed native mobile app.

hope that helps a bit..

KevinHetzelGFL commented 11 months ago

@andregreeff thank you very much!

mbeckenbach commented 11 months ago

@andregreeff Great! Thank you so much. I just had to modify one line on my mac, adding a fallback if wireless or ethernet up dont work.

const serverIp = wirelessIp || ethernetIp || address();

mbeckenbach commented 11 months ago

@andregreeff Did you ever try to use this approach in combination with ngrok?

andregreeff commented 11 months ago

@andregreeff Did you ever try to use this approach in combination with ngrok?

nope, sorry.. I've never used ngrok.

@andregreeff Great! Thank you so much. I just had to modify one line on my mac, adding a fallback if wireless or ethernet up dont work.

const serverIp = wirelessIp || ethernetIp || address();

the process of picking which interface to use is a PITA, especially when trying to do so with as little human input as possible.

just for reference, my Windows 11 work laptop has both a wireless and wired interface, and I have both VMWare and Fortinet VPN installed, each of which add virtual adapters..


disclaimer: this is digressing from the initial "support live reload" topic slightly, but it is relevant in the sense that this is something that needs to be catered for in order to automate the process of running a "serve with live reload" workflow...

so, queryingrequire('os').networkInterfaces() on my PC lists the following interfaces:

but these names can and do vary wildly between different computers, being affected by everything from hardware, host OS, even any additional software that may or may not be installed.

so keeping the "desired interface names" as simple and generic as possible, I opted to test the following:

since my PC is connected to my network by Wi-Fi, my network accessible address, and therefore the address I need to use for capacitor.config.ts, is 192.168.3.4, whereas either specifying the interface public or leaving the function to use it's default (which is also public) returns the address assigned to my "VMware Network Adapter VMnet1", simply because it is the first public network interface with an active connection.


personally, I often switch between both wired and wireless networks, both at home and at work, so in all of my environments checking the Wi-Fi interface first and Ethernet interface second "Just Works" 99% of the time.

tl;dr: the only truly "safe" way to handle this, is to prompt the user to select an interface to use at runtime..

with all that said, I'm not familiar enough with Nx executors to set up a flow like this.. which I imagine would go along the lines of:

the reason I highlight the extra points with the Angular and Capacitor commands here, is that although they can be executed in parallel with nx run-many or the commands array in project.json or even a nx run app:ng-serve && nx run app:cap-run script in your package.json, you may end up with both of these processes running in a terminal window that does not listen to user input.

furthermore, even if it does, their output is dumped into the same window, meaning you probably won't even see that npx is asking for permission to install nx in order to run the required command, simply because that one line is now 4 screens-worth back in your terminal output.

anyways, this is just a highlight of the issues I've run into over the past 2 weeks while trying to get this working in my Nx workspace. I'm very far from being an Nx-ninja, but the most reliable way I've found to do this (so far), is the two-terminal approach I described at the end of my previous comment.

LennonReid commented 11 months ago

I use the command:

cd your app directory run npx ionic capacitor run android --livereload --external --project=your app name

andregreeff commented 11 months ago

I use the command:

cd your app directory run npx ionic capacitor run android --livereload --external --project=your app name

ok sure, but take note that the --project argument is part of Ionic's own "multi-app projects" compatible configuration, as described here: https://ionicframework.com/docs/cli/configuration#multi-app-projects

but this here is where we run into a few potentially sticky points:

most importantly, the apps generated by @nxext/ionic-angular still contain their own individual ionic.config.json files, so getting this to work in our repos (considering this is the @nxext scoped packages workspace) will require a manual configuration change for each generated app.

furthermore, this assumes that you either have NPM workspaces configured in order to take advantage of the root node_modules folder, or that each application project has it's own node_modules folder.

personally I have not had much luck with using NPM's new "workspaces" concept in a Nx workspace, not sure why.. but also this is quite probably why the @nxext/capacitor executors call an npm install inside the workspace app project at the start of virtually every cap command, and then remove the node_modules folder at the end.

tl;dr: trying to use Ionic's own "multi-app workspace" feature for this feels like one giant hack. or at the very least, like it would require a crazy number of smaller hacks in order to "just work".