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

[Question] How to use it for shared code #275

Open
csimpi opened this issue Jul 11, 2021 · 26 comments
Open

[Question] How to use it for shared code #275

csimpi opened this issue Jul 11, 2021 · 26 comments

Comments

@csimpi
Copy link
Contributor

csimpi commented Jul 11, 2021

Hi,
our app is an {N} Angular / Web + Android hybrid app and has been upgraded to NativeScript 8. It stopped working. On Discord I've got the info the schematics are not supported anymore and I have to use xplat if I want to maintain the shared code.

I've created a new workspace added xplat and now I'm stuck because:

  • all of the docs and youtube podcasts are outdated even the folder structure is different (xplat folder went to libs)
  • there's no information on how to use it for shared code
  • nothing is working the way as NX tutorials explain, for example nx g lib --parentModule=xy doesn't work, and when I add a library with --routing and --lazy the routes don't appear in the parent project
  • I can't add a component below a parent lib (--project=main-ui), I'm getting error reports about I have to use platform prefix which I don't understand

Can I get some support here? I just need an example where I can see how the code sharing works between nativescript and web (angular) project.

Another questions:

  • What is the difference between ng g component and ng g feature?
  • How can I add subfolders? nx g feature doesn't have --directory parameter? I would like to organize my features, for example, pages would go into the pages folder.
  • what is the default shared.module for? Why UI module has been wrapped into shared.module? Why not importing UI module itself directly?
@NathanWalker
Copy link
Member

NathanWalker commented Jul 11, 2021

@csimpi Hi, When coming from @nativescript/schematics (or nativescript-schematics) there's a few important things to understand on the surface:

In @nativescript/schematics (or nativescript-schematics) you might have something like this:

src/app/account/account.component.html
src/app/account/account.component.tns.html
src/app/account/account.component.tns.ts
src/app/account/account.component.ts

With Nx+xplat it would look like this:

libs/xplat/features/src/lib/ui/base/account.base-component.ts
libs/xplat/nativescript/features/src/lib/ui/components/account/account.component.html
libs/xplat/nativescript/features/src/lib/ui/components/account/account.component.ts
libs/xplat/web/features/src/lib/ui/components/account/account.component.html
libs/xplat/web/features/src/lib/ui/components/account/account.component.ts

The above uses a unique option createBase (which is optional and off by default) but used when you want to drive platform decorated components with one singular class logic (account.base-component). This component structure can be generated in your own workspace with this command (using dry-run so you can try it and see result without actually generating):

nx generate @nstudio/angular:component --name=account --createBase --platforms=nativescript,web --no-interactive --dry-run

This allows any app in the Nx workspace to simply consume that component however it needs. For example, given you have 1 web app and 1 nativescript app in your workspace (created using nx g app and choosing the appropriate platform), you would have a structure of apps like this:

apps
  web-myapp
  nativescript-myapp

Each of those apps can import the component from the specific location cleanly:

For apps/web-myapp, it could import that component like this:

import { AccountComponent } from '@yourscope/xplat/web/features';

For apps/nativescript-myapp, it could import that component like this:

import { AccountComponent } from '@yourscope/xplat/nativescript/features';

Same component name, no custom file extensions, natural folder organization for code, clarity in what you are dealing with (what platform is this thing?) leading to no custom file extension loaders, no overhead in maintenance and natural webpack handling.

There's more I can share later but that's one of biggest things to understand to start with.

@NathanWalker
Copy link
Member

NathanWalker commented Jul 12, 2021

What is the difference between ng g component and ng g feature?

ng g component = Component which is generated into a module

  • Use component when you just want to generate a component to use which by default will auto target ui feature or a feature of your choice provided you had created a feature for it (more on that as follows...).

ng g feature = Generation of an entirely new module with or without a component attached to it

  • Use feature when you want to generate an Angular module to generate components into.

How can I add subfolders? nx g feature doesn't have --directory parameter? I would like to organize my features, for example, pages would go into the pages folder.

When using the feature generator to create new page routes you can do this:

> nx generate @nstudio/angular:feature --name=page-account --onlyProject --projects=web-myapp --routing --no-interactive --dry-run

CREATE apps/web-myapp/src/app/features/page-account/page-account.module.ts
CREATE apps/web-myapp/src/app/features/page-account/index.ts
CREATE apps/web-myapp/src/app/features/page-account/components/index.ts
CREATE apps/web-myapp/src/app/features/page-account/components/page-account/page-account.component.html
CREATE apps/web-myapp/src/app/features/page-account/components/page-account/page-account.component.ts
UPDATE apps/web-myapp/src/app/app.routing.ts

That would auto annotate the app's routing and auto configure a route by that name page-account (you can manually adjust that anytime in the routes yourself if you'd like). The same thing works for NativeScript if you just specify the nativescript app name with the --projects=nativescript-myapp argument.

The key thing about the above feature generation is the onlyProject argument. This directs the generation to an app only. This is important to understand with xplat as the absence of that argument defaults to generating "shared code" in the libs, for example here's a very sophisticated example of shared code generation of a brand new feature "account" which would create web+mobile counterparts along with even a base to drive the default component it attaches to each:

nx generate @nstudio/angular:feature --name=account --createBase --platforms=nativescript,web --no-interactive --dry-run

CREATE libs/xplat/features/src/lib/account/account.module.ts
CREATE libs/xplat/features/src/lib/account/index.ts
CREATE libs/xplat/features/src/lib/account/base/account.base-component.ts
CREATE libs/xplat/features/src/lib/account/base/index.ts
CREATE libs/xplat/nativescript/features/src/lib/account/account.module.ts
CREATE libs/xplat/nativescript/features/src/lib/account/index.ts
CREATE libs/xplat/nativescript/features/src/lib/account/components/index.ts
CREATE libs/xplat/nativescript/features/src/lib/account/components/account/account.component.html
CREATE libs/xplat/nativescript/features/src/lib/account/components/account/account.component.ts
CREATE libs/xplat/web/features/src/lib/account/account.module.ts
CREATE libs/xplat/web/features/src/lib/account/index.ts
CREATE libs/xplat/web/features/src/lib/account/components/index.ts
CREATE libs/xplat/web/features/src/lib/account/components/account/account.component.html
CREATE libs/xplat/web/features/src/lib/account/components/account/account.component.ts
UPDATE libs/xplat/features/src/lib/index.ts
UPDATE libs/xplat/nativescript/features/src/lib/index.ts
UPDATE libs/xplat/web/features/src/lib/index.ts

This creates:

import { AccountModule } from '@yourscope/xplat/web/features';

// and...

import { AccountModule } from '@yourscope/xplat/nativescript/features';

Giving you the ability to immediately start working with your new cross platform feature.

what is the default shared.module for? Why UI module has been wrapped into shared.module? Why not importing UI module itself directly?

This is a great question and easily the most often confused notion of xplat. Since xplat is about sharing code in general and Nx is all about sharing code, the notion of "SharedModule" loses it's meaning in the scope of shared code alone like "libs" since everything in libs is "shared", whether it's called SharedModule or not. So xplat makes a distinction here by naming those "shared SharedModule's" UIModule because in reality that is what they are. UIModule is a sharable module housing ui counterparts that can be imported/exported from any other modules that need to consume those ui pieces (components, directives or pipes).

Each app has it's own "SharedModule" which auto imports the corresponding platform UIModule by default allowing you to just use the generators to begin rapidly building out code while the generators auto annotate the UIModule for you and thus since each app already imports UIModule from the respective correct platform shared code (xplat/web or xplat/nativescript) you can just start using that stuff - with the confidence that there will be no platform bleed or interference because they are neatly isolated and organized already.

The default behavior of the component, directive, pipe generators is to target the ui feature, thus the UIModule for each respective platform as a convenience. It's up to you if you want to generate your own feature modules using the feature generator to direct your shared code generation to attach itself to different features, for example provided the 'account' feature example above, you could generate more components for that feature like this:

nx generate @nstudio/angular:component --name=settings --feature=account --platforms=nativescript,web --no-interactive --dry-run

CREATE libs/xplat/nativescript/features/src/lib/account/components/settings/settings.component.html
CREATE libs/xplat/nativescript/features/src/lib/account/components/settings/settings.component.ts
CREATE libs/xplat/web/features/src/lib/account/components/settings/settings.component.html
CREATE libs/xplat/web/features/src/lib/account/components/settings/settings.component.ts
UPDATE libs/xplat/nativescript/features/src/lib/account/components/index.ts
UPDATE libs/xplat/web/features/src/lib/account/components/index.ts

That SettingsComponent is now immediately usable via AccountModule in any Web and NativeScript app in your workspace without any additional configuration. You just start using it and enhancing/building it out.

I highly recommend using Nx Console with VS Code as it provides a visual interaction tool to all the options the schematics provide (even outside of xplat) so it's insanely helpful in understanding generators, their options, and given it executes in dry-run mode by default, it allows you to try things out and poke around at what other options would do.

Hope that helps clarify a few things.

Screen Shot 2021-07-11 at 6 44 10 PM

@NathanWalker
Copy link
Member

NathanWalker commented Jul 12, 2021

The other thing worth mentioning is core vs. features.

This concept goes back to a widely talked about topic with regards to Angular specifically - CoreModule vs. SharedModule.
For reference just a few discussions:
https://stackoverflow.com/questions/42695931/angular2-coremodule-vs-sharedmodule
https://levelup.gitconnected.com/where-shall-i-put-that-core-vs-shared-module-in-angular-5fdad16fcecc
https://thetombomb.com/posts/app-core-shared-feature-modules

A well known and fascinating topic with Angular in general and you can find even more references to this concept.

With xplat, the understanding of core vs. feature runs strong as it uses each for the same reasons discussed widely in those references.

CoreModule is imported once into a module stack of an app and used for singleton services and used most often for runtime configuration of various services. xplat specifically uses them to that advantage to configure services for the various platform runtimes, you can see that at play in xplat/web/core -> CoreModule and xplat/nativescript/core -> CoreModule respectively. They both configure shared code for the bottom lowest layer of the architecture xplat/core which is a central location of widely used services across the whole spectrum of cross platform work you may build in a Nx + xplat workspace. This allows developers to use 1 service api with confidence that each platform CoreModule configures that service appropriately for the target runtime in which the app is running, be it web, ios or android.

Everything that's not core is generally a feature and typically organized as such since features are the often optional lazy loaded (or not) portions of code that certainly play a role in any app deployed from the workspace but may not be central to the core operation of any given app. For example, an http interceptor that controls all aspects of every app's interaction with your organizations backend api's would be considered core to all apps within the workspace whereas a SettingsComponent provided by an 'account' feature may only be used in 2 out of 20 apps in the workspace and at that likely lazy loaded, therefore not considered core to the operational architecture of the workspace as a whole.

@csimpi
Copy link
Contributor Author

csimpi commented Jul 12, 2021

@NathanWalker Thank you for the answers, it's clearer now, and I see what is the approach.

What I'd still need to know how to organize features to directories. I understand you recommend using page-* but think of a situation when you have 50+ pages, and any other features, all of them in the same folder... this would make the development almost impossible. IMHO, forcing users to NOT structure their codes into folders feeling nonsense to me. In our current web app we have ~100 components. Not possible to handle the codebase without folders.
I even have to sturcture our pages into sub-subfolder like: pages/auth/login, or pages/dashboard/settings

I'd propose something here:
Why not support / as Angular generators do originally?

Similarly to the page-account, pages/account could work => this should generate the feature in the pages folder, and place AccountModule there (or even PagesAccountModule).

I've tested this and since Angular can handle / in path, it generates the subfolder, BUT the name of the module is wrong:

nx generate @nstudio/angular:feature --name=pages/account --createBase --projects=web-app,nativescript-app --no-interactive --dry-run <

    Could not format /libs/xplat/features/src/lib/pages/account/pages/account.module.ts because '{' expected. (6:19)
      4 |   schemas: [NO_ERRORS_SCHEMA],
      5 | })
    > 6 | export class Pages/accountModule {}
        |                   ^
      7 |
    Could not format /libs/xplat/features/src/lib/pages/account/base/pages/account.base-component.ts because '{' expected. (5:28)
      3 |
      4 | @Directive()
    > 5 | export abstract class Pages/accountBaseComponent extends BaseComponent {
        |                            ^
      6 |   
      7 |   public text: string = 'Pages/account';
      8 |   
    Could not format /libs/xplat/web/features/src/lib/pages/account/pages/account.module.ts because ',' expected. (2:15)
      1 | import { NgModule } from '@angular/core';
    > 2 | import { Pages/accountModule as SharedPages/accountModule } from '@mtp-ui/xplat/features';
        |               ^
      3 | import { UIModule } from '../ui/ui.module';
      4 | import { PAGESACCOUNT_COMPONENTS } from './components';
      5 |
    Could not format /libs/xplat/web/features/src/lib/pages/account/components/index.ts because ',' expected. (1:15)
    > 1 | import { Pages/accountComponent } from './pages/account/pages/account.component';
        |               ^
      2 |
      3 | export const PAGESACCOUNT_COMPONENTS = [
      4 |   Pages/accountComponent
    Could not format /libs/xplat/web/features/src/lib/pages/account/components/pages/account/pages/account.component.ts because ',' expected. (3:15)
      1 | import { Component } from '@angular/core';
      2 |
    > 3 | import { Pages/accountBaseComponent } from '@mtp-ui/xplat/features';
        |               ^
      4 |
      5 | @Component({
      6 |   selector: 'mtp-ui-pages/account',
    Could not format /libs/xplat/nativescript/features/src/lib/pages/account/pages/account.module.ts because ',' expected. (2:15)
      1 | import { NgModule, NO_ERRORS_SCHEMA } from '@angular/core';
    > 2 | import { Pages/accountModule as SharedPages/accountModule } from '@mtp-ui/xplat/features';
        |               ^
      3 | import { UIModule } from '../ui/ui.module';
      4 | import { PAGESACCOUNT_COMPONENTS } from './components';
      5 |
    Could not format /libs/xplat/nativescript/features/src/lib/pages/account/components/index.ts because ',' expected. (1:15)
    > 1 | import { Pages/accountComponent } from './pages/account/pages/account.component';
        |               ^
      2 |
      3 | export const PAGESACCOUNT_COMPONENTS = [
      4 |   Pages/accountComponent
    Could not format /libs/xplat/nativescript/features/src/lib/pages/account/components/pages/account/pages/account.component.ts because ',' expected. (3:15)
      1 | import { Component } from '@angular/core';
      2 |
    > 3 | import { Pages/accountBaseComponent } from '@mtp-ui/xplat/features';
        |               ^
      4 |
      5 | @Component({
      6 |   moduleId: module.id,
CREATE libs/xplat/features/src/lib/pages/account/index.ts
CREATE libs/xplat/features/src/lib/pages/account/pages/account.module.ts
CREATE libs/xplat/features/src/lib/pages/account/base/index.ts
CREATE libs/xplat/features/src/lib/pages/account/base/pages/account.base-component.ts
CREATE libs/xplat/web/features/src/lib/pages/account/index.ts
CREATE libs/xplat/web/features/src/lib/pages/account/pages/account.module.ts
CREATE libs/xplat/web/features/src/lib/pages/account/components/index.ts
CREATE libs/xplat/web/features/src/lib/pages/account/components/pages/account/pages/account.component.html
CREATE libs/xplat/web/features/src/lib/pages/account/components/pages/account/pages/account.component.ts
CREATE libs/xplat/nativescript/features/src/lib/pages/account/index.ts
CREATE libs/xplat/nativescript/features/src/lib/pages/account/pages/account.module.ts
CREATE libs/xplat/nativescript/features/src/lib/pages/account/components/index.ts
CREATE libs/xplat/nativescript/features/src/lib/pages/account/components/pages/account/pages/account.component.html
CREATE libs/xplat/nativescript/features/src/lib/pages/account/components/pages/account/pages/account.component.ts
UPDATE libs/xplat/features/src/lib/index.ts
UPDATE libs/xplat/web/features/src/lib/index.ts
UPDATE libs/xplat/nativescript/features/src/lib/index.ts

UPDATE - Workaround

I didn't test it through but I think adding the below lines to XplatFeatureHelper getTemplateOptions() should do the trick. The dryRun looks good so far, even if I use mixed / and -, for example pages/main-account

/packages/xplat/src/utils/xplat.ts:1196

    const folderParts = options.name.split('/');
    if (folderParts.length > 1) {
      options.name = stringUtils.capitalize(folderParts[folderParts.length - 1]);
    }

Result:

CREATE libs/xplat/features/src/lib/pages/dashboard/main-account/main-account.module.ts
CREATE libs/xplat/features/src/lib/pages/dashboard/main-account/index.ts
CREATE libs/xplat/features/src/lib/main-account/base/main-account.base-component.ts
CREATE libs/xplat/features/src/lib/main-account/base/index.ts
CREATE libs/xplat/web/features/src/lib/main-account/main-account.module.ts
CREATE libs/xplat/web/features/src/lib/main-account/index.ts
CREATE libs/xplat/web/features/src/lib/main-account/components/index.ts
CREATE libs/xplat/web/features/src/lib/main-account/components/main-account/main-account.component.html
CREATE libs/xplat/web/features/src/lib/main-account/components/main-account/main-account.component.ts
CREATE libs/xplat/nativescript/features/src/lib/main-account/main-account.module.ts
CREATE libs/xplat/nativescript/features/src/lib/main-account/index.ts
CREATE libs/xplat/nativescript/features/src/lib/main-account/components/index.ts
CREATE libs/xplat/nativescript/features/src/lib/main-account/components/main-account/main-account.component.html
CREATE libs/xplat/nativescript/features/src/lib/main-account/components/main-account/main-account.component.ts
UPDATE libs/xplat/features/src/lib/index.ts
UPDATE libs/xplat/web/features/src/lib/index.ts
UPDATE libs/xplat/nativescript/features/src/lib/index.ts

csimpi added a commit to Bitmads/xplat that referenced this issue Jul 12, 2021
@NathanWalker
Copy link
Member

@csimpi that makes sense yeah - if you want to post a PR of the branch mention there I could run some tests here and we could try to get out in a patch update. I just need to check it against a few flows.

@csimpi
Copy link
Contributor Author

csimpi commented Jul 13, 2021

@NathanWalker Thank you, PR sent.
I've built a version and the tests passed on my side. Sorry, but I had no time to create some extra tests to check the results but looks good so far, I'll continue the testing now with the locally built version with a real project now.

@s0l4r
Copy link

s0l4r commented Dec 1, 2021

This is a great question with great answers! I was just wondering the same thing. I am just getting into Xplat, seems you have really been thinking when you did the design, great job! You should post these examples on your web @NathanWalker, including the use case with the directories. Thanks for this!

@NathanWalker
Copy link
Member

NathanWalker commented Dec 16, 2021

Thanks @s0l4r - lemme know if you've encountered any other questions past couple days. There's been a lot of evolution with Nx since xplat started and they have a @nrwl/devkit now which is super nice and we're in process of adhering to the new devkit sometime in 2022 which will help streamline more. We're also planning to swap some under hood processes to use other established packages (like @nxtend for ionic handling).

@mohammadrafigh
Copy link

@NathanWalker Is there any advice on how to use Nativescript platform specific files (aka. .android.ts and .ios.ts)? the build fails with current default configurations: map.component.android.ts is missing from the TypeScript compilation.. Adding **/*.android.ts and .d.ts in ts.config didn't fix the problem.

@lostation
Copy link

Hi NS team,

I'm also currently doing my best to finally migrate all my big codebase to brand new nx workspace... To be honest, it's a bit hard ... I don't know where to define yet all my old NS6 big shared core modules and split into features, core etc...

Previously with shared code project on NS6... I used to create helpers files with and without the tns.ts suffix in a very top shared modules.... so it was easy to share an helper for web and another for mobile and inject the proper runtime one into another class constructor etc...

But now I'm a bit stucked with them. I'm searching how I can achieve the same kind of injection behavior but within Nx Workspace between a xplat/web/core/src/lib/helpers/... and xplat/nativescript/core/src/lib/helpers/...

I should find a solution to be able to inject the correct helper into code that is upper than the ns/helpers and web/helpers for instance from features or even xplat/core.

I hope there is an easier solution than create all helperBase abstract classes and uses some kind of InjectionToken stuffs...

If you have advices to handle the helpers files ... ; )

Thanks in advance.
Lo.

@NathanWalker
Copy link
Member

NathanWalker commented Jun 29, 2022

Hi @lostation could you provide an example of one of your helpers? Seeing how you had them setup may help me provide better guidance.

Each platform has a CoreModule which configures services per platform which is where you can provide platform specific behavior. An example of that is provided in the xplat/web/core and xplat/nativescript/core whereby window and language are configured.

@lostation
Copy link

lostation commented Jun 29, 2022

Hi Nathan,

Thanks for your fast answer. I appreciate it.

So for instance with one of my helper so called app.service.ts with a single method called getElementById for the sake of simplicity.

As you know previously with NS6:

I was using a shared core module that holds an helper's folder where we could find files like app.service.tns.ts and app.service.ts and a lot of others...Then in any component constructor, injecting AppService or other, was easy and the right one was selected during compile time. Thanks to suffix .tns for that. I had to only be sure both files contain the same function signatures with the right specific implementation.

Now with NS8 + xPlat architecture:

From xplat/nativescript/core/src/lib/helpers/app.service.ts

static getElementById(id: string)
{
    return Frame.topmost().currentPage.getViewById(id);
}

From xplat/web/core/src/lib/helpers/app.service.ts

static getElementById(id: string)
{
     return document.getElementById(id);
}

Then I have a file used in a shared way, either used by nativescript and web helpers: visual-element.ts

import { AppService } from '../core/helpers/app.service';  <--- previous working import with NS6

export class VisualElement
{
  private id: number;
  width = 0;
  height = 0;

  constructor(x: number, y: number, type: VisualElementType, content: any)
  {
        this.id = ++VisualElement.nextId;
  }

   fitContentSize()
   {
          const contentElement = **AppService**.getElementById(`ve_content_${ this.id }`);
          if (contentElement)
          {
                this.Width = contentElement.offsetWidth;
                this.Height = contentElement.offsetHeight;
          }
    }
}

Where should I put this visual-element.ts file to be shared between folders xplat/nativescript and xplat/web to avoid to duplicate it ? How import the right helper implementation ?

From xplat/features or xplat/core ?

Those places are indeed the shared places between xplat/nativescript and xplat/web...but then again how to import AppService from visual-element.ts as it is only defined, a step lower, from within xplat/nativescript/helpers/xxx and web/helpers/xxx specific folders ?

I have a lot a shared files like that between mobile app and web where I used to import my helpers and NS was gently using the correct one.

Thanks for your time.
Lo.

@lostation
Copy link

Is the NativeScriptConfig and shared attribute is still functioning with NS8 ? And can be use to retrieve the auto .tns.ts suffix discrimination ? between app.service.tns.ts and app.service.ts event from an xplat architecture... ?

Then if so, I could maybe (re)merge all my helpers from within a single xplat/core/helpers folder and if the "Shared" is still functioning NS will do the rest, like NS6 ?

@NathanWalker
Copy link
Member

NathanWalker commented Jun 29, 2022

Very nice thanks, this is what I would do:

Create the broad workspace service helper you will use everywhere

  • create libs/xplat/core/src/lib/services/app.service.ts
import { PlatformHelperToken } from './tokens';

@Injectable({
  providedIn: 'root'
})
export class AppService {
  static PlatformHelper = inject(PlatformHelperToken);
  static getElementById(id: string)
  {
    return AppService.PlatformHelper.getElementById(id);
  }
}

Make sure that's exported from the libs/xplat/core/src/lib/services/index.ts to be usable from the workspace barrel.

  • modify libs/xplat/core/src/lib/services/tokens.ts to include:
export interface IPlatformHelper {
  getElementById(id: string): any; // you can strongly type return type if you'd like
  // you can also include any other helpers you'd like here
}
export const PlatformHelperToken = new InjectionToken<IPlatformHelper>('PlatformHelperToken');

Now just provide each platform's CoreModule

NativeScript

  • update libs/xplat/nativescript/core/src/lib/core.module to provide it:
import { PlatformHelperToken, IPlatformHelper } from '@your-workspace/xplat/core';

export function platformHelperFactory(): IPlatformHelper {
  return {
    getElementById(id: string) {
      return Frame.topmost().currentPage.getViewById(id);
    }
  }
}

@NgModule({
  imports: [
    ...
    CoreModule.forRoot([
      { 
        provide: PlatformHelperToken,
        useFactory: platformHelperFactory
      }

Web

  • update libs/xplat/web/core/src/lib/core.module to provide it:
import { PlatformHelperToken, IPlatformHelper } from '@your-workspace/xplat/core';

export function platformHelperFactory(): IPlatformHelper {
  return {
    getElementById(id: string) {
      return document.getElementById(id);
    }
  }
}

@NgModule({
  imports: [
    ...
    CoreModule.forRoot([
      { 
        provide: PlatformHelperToken,
        useFactory: platformHelperFactory
      }

Now enjoy using AppService everywhere

import { AppService } from '@your-workspace/xplat/core';

// this now works perfect in *any* environment/platform

It's the same thing you had but with bit better separation of concerns not prone to evolutionary changes to static analysis with Angular's compiler or webpack hoopla.

Note: this makes use of Angular 14's new/fantastic inline inject api.

@lostation
Copy link

Ok thanks !

I was also thinking to go into the XplatWindow direction. Then perfect, I'll go to this injection token best practices. Indeed add tokens + factories to each modules to use specific file is great solution. The drawback is maybe that we lost types, for that I will add some interfaces to implement in the services from the core modules.

I have 2 more questions...if you ok.

  1. What's exactly the normal usage or goal of the libs/xplat/features/ because as I understood it, this folder is upper than xplat/nativescript and xplat/web... so it is some kind of a shared top folder above xplat/core... then why not put files or add modules in the xplat/core ?

  2. Now with NS8 I need to use "nativescript.config.ts" from the root of my nativescript-app folder. In my projects I have several brandings or flavors and I want to be able to modify "id", "appResourcesPath" for each of flavors. How can I achieve that ? So override the nativescript.config.ts content before all the build process.

Thanks again.
Lo

@NathanWalker
Copy link
Member

NathanWalker commented Jun 30, 2022

Great questions sure thing.

  1. difference is do you want it to always be part of your bundle or lazy loaded?
    core = fundamental/needed for bootstrap/always part of main loadedbundle
    feature = optional/not needed for bootstrap/usually not part of main loaded bundle

You can add modules to either - which one you add to is dependent upon that ^^ most often (follows the same coremodule vs sharedmodule discussion noted above). Furthermore that is the base level guidance with the architecture - you are free to generate additional Nx libraries in/around xplat architecture as you see fit. We do this in practice, we build our foundation within bounds of xplat architecture which helps delineate the cross platform concerns without worries and then have other nx libraries in/around it for different purposes from project to project as it scales.

  1. You can dynamically control stuff like that all through project.json, eg. Inside apps/nativescript-app/project.json use configurations like this:
{
  "projectType": "application",
  "sourceRoot": "apps/nativescript-app/src",
  "prefix": "lo",
  "targets": {
    "build": {
      "executor": "@nativescript/nx:build",
      "options": {
        "noHmr": true,
        "production": true,
        "uglify": true,
        "release": true,
        "forDevice": true
      },
      "configurations": {
        "prod": {
          "fileReplacements": [
            {
              "replace": "../../libs/xplat/core/src/lib/environments/environment.ts",
              "with": "./src/environments/environment.prod.ts"
            }
          ]
        },
        "uat": {
          "fileReplacements": [
            {
              "replace": "../../libs/xplat/core/src/lib/environments/environment.ts",
              "with": "./src/environments/environment.uat.ts"
            }
          ]
        },
        "qa": {
          "fileReplacements": [
            {
              "replace": "../../libs/xplat/core/src/lib/environments/environment.ts",
              "with": "./src/environments/environment.qa.ts"
            }
          ]
        }
      }
    },
    "ios": {
      "executor": "@nativescript/nx:build",
      "options": {
        "platform": "ios",
        "noHmr": true
      },
      "configurations": {
        "build": {
          "copyTo": "./dist/build.ipa"
        },
        "prod": {
          "combineWithConfig": "build:prod"
        },
        "qa": {
          "combineWithConfig": "build:qa",
          "id": "com.company.thisapp",
          "plistUpdates": {
            "Info.plist": {
              "CFBundleDisplayName": "QA: My App",
              "CFBundleName": "QA: My App"
            }
          }
        },
        "uat": {
          "combineWithConfig": "build:uat",
          "id": "com.company.differentid",
          "plistUpdates": {
            "Info.plist": {
              "CFBundleDisplayName": "UAT: My App",
              "CFBundleName": "UAT: My App"
            }
          }
        }
      }
    },
    "android": {
      "executor": "@nativescript/nx:build",
      "options": {
        "platform": "android",
        "noHmr": true
      },
      "configurations": {
        "build": {},
        "prod": {
          "combineWithConfig": "build:prod"
        },
        "qa": {
          "combineWithConfig": "build:qa",
          "id": "com.company.thisapp",
          "xmlUpdates": {
            "src/main/res/values/strings.xml": {
              "resources": {
                "string": [
                  {
                    "app_name": "QA: My App"
                  },
                  {
                    "title_activity_kimera": "QA: My App"
                  }
                ]
              }
            }
          }
        },
        "uat": {
          "combineWithConfig": "build:uat",
          "id": "com.company.differentid",
          "xmlUpdates": {
            "src/main/res/values/strings.xml": {
              "resources": {
                "string": [
                  {
                    "app_name": "UAT: My App"
                  },
                  {
                    "title_activity_kimera": "UAT: My App"
                  }
                ]
              }
            }
          }
        }
      }
    }
}

That shows several things:

  • dynamically changing info.plist values to modify display app name based on configuration
  • dynamically changing strings.xml for android to change display app name based on configuration
  • dynamically changing app ids based on configuration (and app id you want anytime)

Invoked like this:

npx nx run nativescript-app:ios:uat

That would debug iOS using the UAT configuration which would have the following set (all from project.json):

  • bundle id: com.company.differentid
  • app and display name: UAT: My App

Same configs can be built for release mode as follows:

npx nx run nativescript-app:build:uat --platform=ios

@NathanWalker
Copy link
Member

As far as id and appResourcesPath this actually is one of the biggest reasons why Nx is beneficial over prior schematics direction. Those changing indicate unique app deployments and thus should be clearly delineated/managed to avoid confusion. Each should really be a separate app inside Nx which would simplify the configurations a lot. You would then just invoke any app (with it's own resources and id) anytime, eg:

npx nx run nativescript-app:ios:uat

npx nx run nativescript-another:ios:uat

npx nx run nativescript-yetanother:ios:uat

// on and on...

With Nx, each app can consume/use everything from libs so each app just becomes a shell/deploy target to give it unique id/resources for what you're wanting.

@NathanWalker
Copy link
Member

Lastly keep in mind with latest you can also use this strategy which is also another approach that works well within Nx for various environment level handling:
https://docs.nativescript.org/webpack.html#using-env-files

@NathanWalker
Copy link
Member

NathanWalker commented Jun 30, 2022

There's so many options here I keep recalling more 😁 You can also have multiple nativescript.config.{env}.ts files in a single app and use the flags option of the executor within Nx, for example you can pass --config=nativescript.config.uat.ts to use any config file you want with any run. In project.json you can then use the flags option to specify:

"uat": {
  "combineWithConfig": "build:uat",
  "flags": "--config=nativescript.config.uat.ts",

@lostation
Copy link

Hi Nathan,

Ok ;) thanks a lot for all your advices!

I'm not building yet the apps ui part or env stuffs because I'm still dispatching specific and shared stuffs into xplat architecture... I'm starting to well know this new way of handling files...and start to appreciate it finally!

I'll let you know any further things if any, thanks a lot anyway for all your appreciated help.
Lo.

@lostation
Copy link

lostation commented Jul 8, 2022

Re Nathan,

mmm sorry to bother you again... I've got weird errors from Webpack :(

So now everything should be setup and ready to go for the mobile part ...

I've added a npm script to run from global package.json -> "android:br:dev": "npx nx run nativescript-app:android:br-dev"

From nativescript-app/project.json

    "android": {
      "executor": "@nativescript/nx:build",
      "options": {
        "platform": "android",
        "production": false,
        "uglify": false,
        "release": false,
        "noHmr": true
      },
      "configurations": {
        "build": {
          "copyTo": "./dist/build.apk"
        },
        "br-dev": {
          "combineWithConfig": "build:dev",
          "flags": "--config=ns-br.config.ts --env.sourceMap --env.verbose --env.target=MyTarget
        }, ...
      }
}

From nativescript-app/ns-br.config.ts

export default {
  id: 'com.super.mobileapp',
  appResourcesPath: 'App_Resources',
  android: {
    v8Flags: '--expose_gc',
    markingMode: 'none',
    codeCache: true,
    suppressCallJSMethodExceptions: false,
    id: 'com.super.mobileapp',
  },
  ios: {
    discardUncaughtJsExceptions: false,
  },
  appPath: 'src',
  cli: {
    packageManager: 'npm',
  },
  webpackConfigPath: './webpack-custom.config.js',
} as NativeScriptConfig;

So, it starts my "webpack-custom.config.js" then it means that correct NativeScriptConfig from "flags": "--config=..." is selected and running. That already great!

From "webpack-custom.config.js", I'm just doing some files copy, selected brand resources into the App_Resources and also some version build number update.

module.exports = async (env) => {
    ...
    env.nsReleaseVersion = NS_RELEASE_VERSION;
    env.nsBuildVersion = NS_BUILD_VERSION;

    console.log('\nStart webpack-custom.config.js');
    console.log('\nProjectRoot: ' + __dirname);
    console.log('\nEnv');
    console.log(JSON.stringify(env, (k, v) => {
        switch (('' + k).toLowerCase()) {
            case 'pass':
            case 'password':
            case 'login':
            case 'email':
                return '************';
            default:
                return v;
        }
    }, 4));

    // Copy the selected Brand files for (Android|iOS) into the nativescript default "./App_Resources" folder
    await copyResourcesFiles(env).then(
        ok => { },
        err => {
            console.error(err, 'copyResourcesFiles');
            process.exit();
        }
    );

    // Update app_name from ./i18n/ and set build number to AndroidManifest.xml or Info.plist
    await updateFromDescriptors(env).then(
        ok => { },
        err => {
            console.error(err, 'updateFromDescriptors');
            process.exit();
        }
    );

    // then run the regular webpack.config.js
    const config = webpackConfig(env);

    const customDefine = new webpack.DefinePlugin({
        "global.TNS_WEBPACK": "true",
        "process": "global.process",
        "process.env": {
            login:
                Object.prototype.hasOwnProperty.call(env, 'login')
                    ? JSON.stringify(env.login)
                    : undefined,
            pass:
                Object.prototype.hasOwnProperty.call(env, 'pass')
                    ? JSON.stringify(env.pass)
                    : undefined,
            nsReleaseVersion:
                Object.prototype.hasOwnProperty.call(env, 'nsReleaseVersion')
                    ? JSON.stringify(env.nsReleaseVersion)
                    : undefined,
            nsBuildVersion:
                Object.prototype.hasOwnProperty.call(env, 'nsBuildVersion')
                    ? JSON.stringify(env.nsBuildVersion)
                    : undefined,
        },
    });

    config.plugins.unshift(customDefine);

    const appResourcesFullPath = resolve(__dirname, env.appResourcesPath);
    const customCopy = new CopyWebpackPlugin(
        {
            patterns: [
                { from: "**/*.jpg" },
                { from: "**/*.png" },
                { from: "**/*.json" },
                { from: "**/*.ttf" },
            ]
        }, { ignore: [`${relative(env.appPath, appResourcesFullPath)}/**`] });

    config.plugins.unshift(customCopy);

    return config;
};

So my current errors are:

WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/

ERROR in main
Module not found: Error: Can't resolve './src' in '/Volumes/Data/.../brfrontend/apps/nativescript-app'
resolve './src' in '/Volumes/Data/.../brfrontend/apps/nativescript-app'
  using description file:/Volumes/Data/.../brfrontend/apps/nativescript-app/package.json (relative path: .)
    Field 'browser' doesn't contain a valid alias configuration
    using description file: /Volumes/Data/.../brfrontend/apps/nativescript-app/package.json (relative path: ./src)
      no extension
        Field 'browser' doesn't contain a valid alias configuration
       /Volumes/Data/.../brfrontend/apps/nativescript-app/src is not a file .js
        Field 'browser' doesn't contain a valid alias configuration
        /Volumes/Data/.../brfrontend/apps/nativescript-app/src.js doesn't exist .json
        Field 'browser' doesn't contain a valid alias configuration
        /Volumes/Data/.../brfrontend/apps/nativescript-app/src.json doesn't exist .wasm
        Field 'browser' doesn't contain a valid alias configuration
       /Volumes/Data/.../brfrontend/apps/nativescript-app/src.wasm doesn't exist
      as directory
        existing directory /Volumes/Data/.../brfrontend/apps/nativescript-app/src
          using description file: /Volumes/Data/.../brfrontend/apps/nativescript-app/package.json (relative path: ./src)
            using path:/Volumes/Data/.../brfrontend/apps/nativescript-app/src/index
              using description file: /Volumes/Data/.../brfrontend/apps/nativescript-app/package.json (relative path: ./src/index)
                no extension
                  Field 'browser' doesn't contain a valid alias configuration
                  /Volumes/Data/.../brfrontend/apps/nativescript-app/src/index doesn't exist .js
                  Field 'browser' doesn't contain a valid alias configuration
                  /Volumes/Data/.../brfrontend/apps/nativescript-app/src/index.js doesn't exist .json
                  Field 'browser' doesn't contain a valid alias configuration
                  /Volumes/Data/.../brfrontend/apps/nativescript-app/src/index.json doesn't exist .wasm
                  Field 'browser' doesn't contain a valid alias configuration
                  /Volumes/Data/.../brfrontend/apps/nativescript-app/src/index.wasm doesn't exist

webpack 5.73.0 compiled with 1 error and 1 warning in 1706 ms

But I have a src folder into nativescript-app/...
and also mode seems defined

from --env.verbose printed infos, I can see that mode is declared in Webpack

[@nativescript/webpack] Info: 
 {
  mode: 'development',
...

If I remove the attribute webpackConfigPath from NativeScriptConfig... The warning and error disappear... ???

Can we still use custom webpackConfigPath within Webpack and NS8 ? Or maybe I missed something...probably.

Thanks for your precious help.
Lo.

@lostation
Copy link

No in fact even without webpackConfigPath from NativeScriptConfig... The warning and error are still there...

Module not found: Error: Can't resolve './src' in '/Volumes/Data/project/apps/nativescript-app'

I don't really know why ./src is not resolvable....

And even WTH is that Field 'browser' doesn't contain a valid alias configuration

Please help, if you have any hint...

@lostation
Copy link

Waw... It took me days to find out what happened...

!!! async keyword is breaking all webpack build process and it probably takes then the by default webpack config ... as the async is not yet resolved.

In that webpack.config.js, previously with NS6 and the custom config path (now from NativeScriptConfig), that was working and I was able to make some files maintenance, set versions, clean and copy stuffs...before the real webpack was called.

So from nativescript-app/webpack.config.js:

  • WRONG: module.exports = async (env) => { ...
  • GOOD: module.exports = (env) => { ...

I have also changed a bit the chainWebpack based from the webpack docs

  webpack.chainWebpack((config) => {
   ...
    config.plugin('DefinePlugin').tap(args => {
      Object.assign(args[0], {
        'global.isProduction': !!env.production,
        'global.login': Object.prototype.hasOwnProperty.call(env, 'login')
          ? JSON.stringify(env.login)
          : undefined,
        'global.pass': Object.prototype.hasOwnProperty.call(env, 'pass')
          ? JSON.stringify(env.pass)
          : undefined,
        'global.nsReleaseVersion': Object.prototype.hasOwnProperty.call(env, 'nsReleaseVersion')
          ? JSON.stringify(env.nsReleaseVersion)
          : undefined,
        'global.nsBuildVersion': Object.prototype.hasOwnProperty.call(env, 'nsBuildVersion')
          ? JSON.stringify(env.nsBuildVersion)
          : undefined
      });
      return args
    });

  });

  webpack.Utils.addCopyRule("**/*.jpg");
  webpack.Utils.addCopyRule("**/*.png");
  webpack.Utils.addCopyRule("**/*.json");
  webpack.Utils.addCopyRule("**/*.ttf");

return webpack.resolveConfig();

Hope this help someone else...

@lostation
Copy link

About local plugins to handlefrog nx and app...

I wonder what is the best practices with local npm custom nativescript plugins.
Previously in NS6, I've used root folder called -> ns-libs/... that contained my modified plugins for instance nativescript-markdown or mqtt stuffs or other custom plugins...

Should I put that plugin folder inside the nativescript app level or workspace level ?
If I put at workspace level...every project can have access to...I mean even the nativescript core features ...
But If I put it at app level..then how achieve some kind of shared import from nativescript core features ?

I've tried to link all the plugins index.d.ts from the workspace references.d.ts...but that doesn't seem to work.

I've also seen that there is "@brfrontend/xplat-scss": "file:libs/xplat/scss/src" from workspace package.json dependencies... Maybe should I do the same for the local plugins located at ns app level ?

What is the best way to handle that from nx point of view @NathanWalker ?

Thanks

@lostation
Copy link

Ah and I tried with that plugins folder from the workspace root...but then some defined java flies are not compiled correctly and I've got some static error with interface declared inside the java class package name... But not from the ns app level, that seem to compile java to byte code as it was before....So seems better to put that custom things inside ns app level...It has the benefit to not pollute the root folder.

@NathanWalker
Copy link
Member

@lostation when possible might drop in Discord:
https://nativescript.org/discord
might be easier to chat there (#angular channel)
You can setup workspace using npm or yarn workspaces - which you use within Nx for dependency management can affect how you want to set your package. The rule of thumb is that you want to add all dependencies to root package.json for sure. Then for any NativeScript app in the workspace you want it’s own package to contain NativeScript dependencies. That will ensure the cli picks up and builds the native dependencies into the app (webpack naturally picks up all the other standard js only stuff)

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

No branches or pull requests

5 participants