Marmicode
Blog Post
Younes Jaaidi

Versatile Angular Style brings Modern Dev Tools to Angular

by Younes Jaaidi โ€ข 
Dec 7, 2022 โ€ข 12 minutes
Versatile Angular Style brings Modern Dev Tools to Angular

As surprising as this might sound, Angular is one of the easiest frameworks to integrate with modern dev tools out of the box ... but only if you follow the rules:

Following these rules will instantly open the gates of tools like Vite, Vitest, SWC, Playwright Component Testing... and maybe Turbopack.

๐Ÿ‡ Dev Tools Evolve & Proliferate

While there are chances that some of you might not adhere to the Versatile Angular Style, I am sure that we will agree on one thing: .

Let's start with build tools, skip the Grunt & Gulp era and focus on the post-Webpack period.

โฉ esbuild & โšก๏ธ Vite

The first game changer was esbuild. In fact, why not use Go to transform code? Then why not mix esbuild and modern browser features to instantly start a dev server and serve files on-demand? Well, that's when Vite joins the party.

It became so popular that some very useful tools only support Vite right now and others are simply built on top of it .

๐Ÿค” ... but what about Angular support?

Since Angular 14, it is possible to opt-in to a .

{
  // Nx
  "executor": "@angular-devkit/build-angular:browser-esbuild",
  // Angular CLI
  "builder": "@angular-devkit/build-angular:browser-esbuild",
}

This make it easier to add Angular support to build & dev tools like Vitest, Vitest, Vite-based Cypress Component Testing, Playwright Component Testing etc...

๐Ÿฆ€ mind the Rustaceans!

In the meantime, on some Rusty beach, Rust-based builders are starting to proliferate. It first started with SWC, and now, here comes Turbopack.

How can all dev tools catch up?

...and what about less popular tools like WMR?

...finally, while the last wave brought Rustaceans, how can we predict what the next wave is holding for us?

๐Ÿ‘ด๐Ÿผ Wait or Act ๐Ÿ’ช

There are different ways of handling this situation. We could just wait for official or community-driven initiatives to add Angular support on all these kinds of tools , or .

Faster Build Twitter Poll

As a starting point, I decided to run a little poll to compare the importance of development vs. production build speed. While of course, the importance of deployment (production build) is not neglectable and shouldn't be ignored when considering Developer eXperience, the local feedback loop (development build) seems to be more important.

This makes sense of course as .

Let's focus on development builds & dev tools then.

๐Ÿช„ JIT is the Golden Ticket

Thanks to JIT (Just-in-Time) mode, as it can be used in any context and with almost no code transformation . Cf.

As a matter of fact, the day the following ECMAScript propositions get adopted:

what would prevent us from running Angular code using JIT, without any transformer while keeping our code AOT compatible?

๐Ÿ“ Use inline template & styles

The first thing that all builders have to handle is resolving templateUrl and styleUrls as they can't be resolved at runtime. We can save some hassle for all tools by simply using inline and .

@Component({
  ...
  template: `<h1>Hello!</h1>`,
  styles: [
    `
      :host {
        background: purple;
        color: white;
      }
    `
  ]
})
class GreetingsComponent {}

Also, if you've already been seduced by SFAM = (SFC + ' ), now that standalone components are here (and ), it just makes even more sense.

๐Ÿ›‘ Avoid Style Preprocessors

As the styles will be discovered at runtime, we'll have to avoid any style preprocessors . This means no Sass, nor Tailwind directives at the component level.

That said, you can still use Sass & Tailwind directives at the global level as it is easier to set up with different build tools. For instance, we could use different entry points .

๐Ÿ’‰ Use inject() instead of Traditional DI

The last major issue is supporting :

@Component(...)
class GreetingsComponent {
  constructor(greetings: GreetingsService) {}
}

At runtime, Angular needs to access the GreetingsService symbol as it is the key to allowing the resolution of the dependency. For this to work, Angular requires TypeScript to emit some metadata .

This metadata is only available when using the TypeScript compiler with the emitDecoratorMetadata option enabled.

The problem with this requirement is that emitDecoratorMetadata is not supported and will probably never be supported on some builders like esbuild for .

Yes, we could implement an esbuild plugin that compiles TypeScript and emits decorator metadata but we would lose the whole performance benefit of using esbuild... and the lightness of having Angular working with esbuild out of the box.

Prefer inject() to @Inject()

The first workaround we can think of is using the .

@Component(...)
class GreetingsComponent {
  constructor(@Inject(GreetingsService) greetings: GreetingsService) {}
}

In fact, using @Inject(), Angular doesn't need to emit any decorator metadata... but this approach has two major drawbacks:

The best alternative is the inject() function.

Since Angular 14, we can use the new .

const GREETINGS_TOKEN = new InjectionToken<GreetingsService>('GREETINGS');

@Component(...)
class GreetingsComponent {
  greetings = inject(GreetingsService); // GreetingsService
  greetingsWithToken = inject(GREETINGS_TOKEN); // GreetingsService
  greetingsOptional = inject(GreetingsService, {optional: true}); // GreetingsService | null
}

๐ŸŽ’ Use Standalone Components

While this is not mandatory for most tools, Standalone Components turn out to be easier to integrate with modern dev tools as we only need a single entry point to pinpoint a component .

For instance, 's mount() function experimental support for React, Solid, Svelte, Vue is expecting one parameter that can be either a component type symbol or JSX. To make it work with Angular modules, this would require a couple of hacks but with Standalone Components, this works almost out of the box without changing the way Playwright Component Testing works.

๐Ÿคน Demo

A few months ago, my friend and I ... then I think that we kind of got distracted by Angular 14's release ๐Ÿ˜…...

... until the , so I finally decided to take some time and set up a demo combining everything:

Wanna try first? Check it out here:

โšก๏ธ Vite

Vite worked out of the box with Versatile Angular Style with nothing more than the configuration below. that allows us to import prebuilt Angular Material styles (Cf. ).

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  root: 'src',
  resolve: {
    conditions: ['style'],
  },
});

โœ… Vitest

Vitest has an issue with Zone.js... or should I say, Zone.js has an issue with Node.js & ES Modules dynamic imports .

zoneless & fine

By the way, do we really need zones in tests even if our app is not totally zoneless yet? Well, in most cases, we don't:

Sadly, there is no simple way of using TestBed without zone.js but this should hopefully be fixed soon. Meanwhile, the workaround is using a NoopZone as a no-op alternative to Zone .

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    setupFiles: ['src/test-setup.ts']
  },
});
// test-setup.ts
import './app/testing/noop-zone';

import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting,
} from '@angular/platform-browser-dynamic/testing';
import { getTestBed } from '@angular/core/testing';

getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);

๐Ÿฆ€ Jest + SWC

Thanks to @swc/jest, Jest + SWC works out of the box with this configuration:

// jest.config.ts
module.exports = {
  setupFilesAfterEnv: ["<rootDir>/src/test-setup.ts"],
  testEnvironment: "jsdom",
  transform: {
    "^.+\\.m?(t|j)sx?$": ["@swc/jest"],
  },
  /* We could get rid of this if we switch to ESM. */
  transformIgnorePatterns: ["/node_modules/(?!(@angular/)"],
};

๐ŸŽญ Playwright Component Testing

Vite, Vitest, and Jest + SWC integration went so fine that it gave me the courage to go a bit further.

Yes! !

The way Playwright Component Testing works is a bit different than how Jest, Vitest, or Cypress Component Testing work because the test's code is running in Node.js while the component is running in the browser. This is the reason why we still need a specific integration for Angular and that is how was born. We'll be sharing more about this in a future blog post, so stay tuned to get notified when it is ready.

Note that using Versatile Angular Style, Angular is the only framework/library, supported by Playwright Component Testing, that doesn't need any plugin, or transformer.

๐Ÿข Progressive Migration to Versatile Angular Style

Don't panic! You don't have to go through a big bang migration. You can progressively start migrating to Versatile Angular Style whenever you bring some changes to a component for example ... or maybe someone could help the community with a ? ๐Ÿ˜‰

Also, if you are using , you can easily provide different build & test configurations for your apps and libraries so you can migrate and focus on the most crucial parts first.

If for some reason, you are temporarily stuck using an Angular version older than 14, then you can use ษตษตdirectiveInject() as a replacement for inject() meanwhile you migrate to a more recent version.


๐Ÿ”— Links

๐Ÿ“ฐ

๐Ÿ’ป

๐Ÿ’ฌ