From 6fe9dfe5c83f5730f73a417a9d85678e2fa61d4d Mon Sep 17 00:00:00 2001
From: Dmitriy Stepanenko <33101123+dmitry-stepanenko@users.noreply.github.com>
Date: Sun, 17 Sep 2023 15:26:47 +0300
Subject: [PATCH] feat(qwik-nx): preliminary implementation of angular
integration (#197)
---
packages/qwik-nx/generators.json | 6 +
.../files/demo/index.tsx.template | 16 ++
.../files/material/index.tsx.template | 113 ++++++++++
.../integrations/angular-in-app/generator.ts | 86 ++++++++
.../integrations/angular-in-app/schema.d.ts | 5 +
.../integrations/angular-in-app/schema.json | 31 +++
.../components/counter.component.ts.template | 53 +++++
.../angular/files/demo/index.ts.template | 8 +
.../components/button.component.ts.template | 23 ++
.../components/input.component.ts.template | 32 +++
.../components/slider.component.ts.template | 50 +++++
.../table/table.component.html.template | 47 +++++
.../table/table.component.scss.template | 17 ++
.../table/table.component.ts.template | 51 +++++
.../integration-files/index.ts.template | 16 ++
.../material/styles/styles.scss.template | 36 ++++
packages/qwik-nx/src/utils/angular/init.ts | 198 ++++++++++++++++++
packages/qwik-nx/src/utils/versions.ts | 9 +-
18 files changed, 795 insertions(+), 2 deletions(-)
create mode 100644 packages/qwik-nx/src/generators/integrations/angular-in-app/files/demo/index.tsx.template
create mode 100644 packages/qwik-nx/src/generators/integrations/angular-in-app/files/material/index.tsx.template
create mode 100644 packages/qwik-nx/src/generators/integrations/angular-in-app/generator.ts
create mode 100644 packages/qwik-nx/src/generators/integrations/angular-in-app/schema.d.ts
create mode 100644 packages/qwik-nx/src/generators/integrations/angular-in-app/schema.json
create mode 100644 packages/qwik-nx/src/utils/angular/files/demo/components/counter.component.ts.template
create mode 100644 packages/qwik-nx/src/utils/angular/files/demo/index.ts.template
create mode 100644 packages/qwik-nx/src/utils/angular/files/material/integration-files/components/button.component.ts.template
create mode 100644 packages/qwik-nx/src/utils/angular/files/material/integration-files/components/input.component.ts.template
create mode 100644 packages/qwik-nx/src/utils/angular/files/material/integration-files/components/slider.component.ts.template
create mode 100644 packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.html.template
create mode 100644 packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.scss.template
create mode 100644 packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.ts.template
create mode 100644 packages/qwik-nx/src/utils/angular/files/material/integration-files/index.ts.template
create mode 100644 packages/qwik-nx/src/utils/angular/files/material/styles/styles.scss.template
create mode 100644 packages/qwik-nx/src/utils/angular/init.ts
diff --git a/packages/qwik-nx/generators.json b/packages/qwik-nx/generators.json
index 3478195..f8915f9 100644
--- a/packages/qwik-nx/generators.json
+++ b/packages/qwik-nx/generators.json
@@ -89,6 +89,12 @@
"factory": "./src/generators/integrations/deno/generator",
"schema": "./src/generators/integrations/deno/schema.json",
"description": "Qwik City Deno adaptor allows you to hook up Qwik City to a Deno server"
+ },
+ "angular-in-app": {
+ "factory": "./src/generators/integrations/angular-in-app/generator",
+ "schema": "./src/generators/integrations/angular-in-app/schema.json",
+ "description": "angular-in-app generator",
+ "hidden": true
}
}
}
diff --git a/packages/qwik-nx/src/generators/integrations/angular-in-app/files/demo/index.tsx.template b/packages/qwik-nx/src/generators/integrations/angular-in-app/files/demo/index.tsx.template
new file mode 100644
index 0000000..bc6aa57
--- /dev/null
+++ b/packages/qwik-nx/src/generators/integrations/angular-in-app/files/demo/index.tsx.template
@@ -0,0 +1,16 @@
+import { component$ } from '@builder.io/qwik';
+import type { DocumentHead } from '@builder.io/qwik-city';
+import { AngularCounterComponent } from '../../integrations/angular';
+
+export default component$(() => {
+ return (
+ <>
+
Qwik/Angular demo
+
+ >
+ );
+});
+
+export const head: DocumentHead = {
+ title: 'Qwik Angular',
+};
diff --git a/packages/qwik-nx/src/generators/integrations/angular-in-app/files/material/index.tsx.template b/packages/qwik-nx/src/generators/integrations/angular-in-app/files/material/index.tsx.template
new file mode 100644
index 0000000..63ae84e
--- /dev/null
+++ b/packages/qwik-nx/src/generators/integrations/angular-in-app/files/material/index.tsx.template
@@ -0,0 +1,113 @@
+import { component$, useSignal } from '@builder.io/qwik';
+import type { DocumentHead } from '@builder.io/qwik-city';
+import {
+ MaterialSlider,
+ MaterialButton,
+ type ButtonComponentProps,
+ MaterialTable,
+ type TableUserData,
+} from '../../integrations/angular';
+
+export default component$(() => {
+ const show = useSignal(false);
+ const count = useSignal(0);
+ const btnColor = useSignal('primary');
+ const users = useSignal(Array.from({ length: 100 }, (_, k) => createNewUser(k + 1)));
+
+ return (
+
+
+ Welcome to Qwik Angular⚡️
+
+
+
+
+
+ {
+ count.value = value;
+ }}
+ />
+
+ alert('click')}>
+ Slider is {count.value}
+
+
+ {
+ show.value = true;
+ }}
+ >
+ Show table
+
+
+ {show.value && }
+
+
+ );
+});
+
+export const head: DocumentHead = {
+ title: 'Qwik Angular',
+};
+
+/** Builds and returns a new User. */
+function createNewUser(id: number): TableUserData {
+ /** Constants used to fill up our data base. */
+ const FRUITS: string[] = [
+ 'blueberry',
+ 'lychee',
+ 'kiwi',
+ 'mango',
+ 'peach',
+ 'lime',
+ 'pomegranate',
+ 'pineapple',
+ ];
+ const NAMES: string[] = [
+ 'Maia',
+ 'Asher',
+ 'Olivia',
+ 'Atticus',
+ 'Amelia',
+ 'Jack',
+ 'Charlotte',
+ 'Theodore',
+ 'Isla',
+ 'Oliver',
+ 'Isabella',
+ 'Jasper',
+ 'Cora',
+ 'Levi',
+ 'Violet',
+ 'Arthur',
+ 'Mia',
+ 'Thomas',
+ 'Elizabeth',
+ ];
+ const name =
+ NAMES[Math.round(Math.random() * (NAMES.length - 1))] +
+ ' ' +
+ NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) +
+ '.';
+
+ return {
+ id: id.toString(),
+ name: name,
+ progress: Math.round(Math.random() * 100).toString(),
+ fruit: FRUITS[Math.round(Math.random() * (FRUITS.length - 1))],
+ };
+}
diff --git a/packages/qwik-nx/src/generators/integrations/angular-in-app/generator.ts b/packages/qwik-nx/src/generators/integrations/angular-in-app/generator.ts
new file mode 100644
index 0000000..fde115d
--- /dev/null
+++ b/packages/qwik-nx/src/generators/integrations/angular-in-app/generator.ts
@@ -0,0 +1,86 @@
+import {
+ formatFiles,
+ generateFiles,
+ joinPathFragments,
+ ProjectType,
+ readProjectConfiguration,
+ Tree,
+} from '@nx/devkit';
+import * as path from 'path';
+import { AngularInAppGeneratorSchema } from './schema';
+import { angularInit } from '../../../utils/angular/init';
+
+interface NormalizedSchema extends AngularInAppGeneratorSchema {
+ sourceRoot: string;
+ projectRoot: string;
+ projectType: ProjectType;
+}
+
+function normalizeOptions(
+ tree: Tree,
+ options: AngularInAppGeneratorSchema
+): NormalizedSchema {
+ const projectConfig = readProjectConfiguration(tree, options.project);
+
+ return {
+ ...options,
+ installMaterialExample: options.installMaterialExample !== false,
+ sourceRoot: projectConfig.sourceRoot ?? projectConfig.root + '/src',
+ projectRoot: projectConfig.root,
+ projectType: projectConfig.projectType!,
+ };
+}
+
+function addFiles(tree: Tree, normalizedOptions: NormalizedSchema): void {
+ const filePath = normalizedOptions.installMaterialExample
+ ? 'material'
+ : 'demo';
+ generateFiles(
+ tree,
+ path.join(__dirname, 'files', filePath),
+ joinPathFragments(normalizedOptions.sourceRoot, 'routes/angular'),
+ {}
+ );
+}
+
+export async function angularInAppGenerator(
+ tree: Tree,
+ schema: AngularInAppGeneratorSchema
+) {
+ const normalizedOptions = normalizeOptions(tree, schema);
+
+ if (normalizedOptions.projectType !== 'application') {
+ throw new Error(
+ `Only applications are supported, "${normalizedOptions.project}" is a library.`
+ );
+ }
+
+ const demoFilePath = joinPathFragments(
+ normalizedOptions.sourceRoot,
+ 'integrations/angular'
+ );
+
+ if (tree.exists(demoFilePath)) {
+ throw new Error(
+ `Looks like angular integration has already been configured for ${normalizedOptions.project}. "${demoFilePath}" already exists.`
+ );
+ }
+
+ const initCallback = angularInit(tree, {
+ demoFilePath: joinPathFragments(
+ normalizedOptions.sourceRoot,
+ 'integrations/angular'
+ ),
+ installMaterialExample: !!normalizedOptions.installMaterialExample,
+ projectRoot: normalizedOptions.projectRoot,
+ isApp: true,
+ });
+ addFiles(tree, normalizedOptions);
+ if (!normalizedOptions.skipFormat) {
+ await formatFiles(tree);
+ }
+
+ return initCallback;
+}
+
+export default angularInAppGenerator;
diff --git a/packages/qwik-nx/src/generators/integrations/angular-in-app/schema.d.ts b/packages/qwik-nx/src/generators/integrations/angular-in-app/schema.d.ts
new file mode 100644
index 0000000..dcdb775
--- /dev/null
+++ b/packages/qwik-nx/src/generators/integrations/angular-in-app/schema.d.ts
@@ -0,0 +1,5 @@
+export interface AngularInAppGeneratorSchema {
+ project: string;
+ installMaterialExample?: boolean;
+ skipFormat?: boolean;
+}
diff --git a/packages/qwik-nx/src/generators/integrations/angular-in-app/schema.json b/packages/qwik-nx/src/generators/integrations/angular-in-app/schema.json
new file mode 100644
index 0000000..5e96e91
--- /dev/null
+++ b/packages/qwik-nx/src/generators/integrations/angular-in-app/schema.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "$id": "AngularInApp",
+ "title": "",
+ "type": "object",
+ "properties": {
+ "project": {
+ "type": "string",
+ "description": "Name of the project to add Angular integration to",
+ "$default": {
+ "$source": "argv",
+ "index": 0
+ },
+ "x-prompt": "Name of the project to add Angular integration to"
+ },
+ "installMaterialExample": {
+ "type": "boolean",
+ "description": "Add dependencies for the Angular Material and qwikified example component, that uses it",
+ "x-priority": "important",
+ "default": true,
+ "x-prompt": "Do you want to have Angular Material example installed?"
+ },
+ "skipFormat": {
+ "description": "Skip formatting files.",
+ "type": "boolean",
+ "x-priority": "internal",
+ "default": false
+ }
+ },
+ "required": ["project"]
+}
diff --git a/packages/qwik-nx/src/utils/angular/files/demo/components/counter.component.ts.template b/packages/qwik-nx/src/utils/angular/files/demo/components/counter.component.ts.template
new file mode 100644
index 0000000..2a9a72d
--- /dev/null
+++ b/packages/qwik-nx/src/utils/angular/files/demo/components/counter.component.ts.template
@@ -0,0 +1,53 @@
+import { Component, EventEmitter, Input, Output, type OnInit } from '@angular/core';
+import type { QwikifiedComponentProps, WithRequiredProps } from '@qwikdev/qwik-angular';
+
+type CounterComponentInputs = 'initialCountValue' | 'heading';
+
+type CounterComponentOutputs = 'countChanged';
+
+type RequiredPropValues = 'initialCountValue';
+
+// using utility types to assemble a type object for qwikified CounterComponent
+// that has all inputs and typed output handlers of Angular CounterComponent
+type OptionalCounterComponentProps = QwikifiedComponentProps<
+ CounterComponent,
+ CounterComponentInputs,
+ CounterComponentOutputs
+>;
+
+// also marking "initialCountValue" as required and exporting the final type
+export type CounterComponentProps = WithRequiredProps<
+ OptionalCounterComponentProps,
+ RequiredPropValues
+>;
+
+@Component({
+ selector: 'app-angular-counter',
+ template: `
+
+
{{ heading }}
+
{{ count }}
+
+
+ `,
+ styles: [`.wrapper { display: flex; flex-direction: column; align-items: center; }`],
+ standalone: true
+})
+export class CounterComponent implements OnInit {
+ @Input() initialCountValue: number = 0;
+ @Input() heading = 'Simple Angular Counter';
+
+ @Output() readonly countChanged = new EventEmitter();
+
+ private count: number;
+
+ ngOnInit(): void {
+ this.count = this.initialCountValue;
+ }
+
+ handleClick(): void {
+ this.count++;
+ this.countChanged.emit(this.count);
+ console.log(`Count: ${this.count}`);
+ }
+}
diff --git a/packages/qwik-nx/src/utils/angular/files/demo/index.ts.template b/packages/qwik-nx/src/utils/angular/files/demo/index.ts.template
new file mode 100644
index 0000000..ce22b57
--- /dev/null
+++ b/packages/qwik-nx/src/utils/angular/files/demo/index.ts.template
@@ -0,0 +1,8 @@
+import { qwikify$ } from '@qwikdev/qwik-angular';
+import { type CounterComponentProps, CounterComponent } from './components/counter.component';
+
+export const AngularCounterComponent = qwikify$(CounterComponent, {
+ eagerness: 'hover',
+});
+
+export { CounterComponentProps };
diff --git a/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/button.component.ts.template b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/button.component.ts.template
new file mode 100644
index 0000000..32c8890
--- /dev/null
+++ b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/button.component.ts.template
@@ -0,0 +1,23 @@
+import { Component, Input } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import type { QwikifiedComponentProps } from '@qwikdev/qwik-angular';
+
+type ButtonComponentInputProps = 'color';
+
+export type ButtonComponentProps = QwikifiedComponentProps<
+ ButtonComponent,
+ ButtonComponentInputProps
+>;
+
+@Component({
+ imports: [MatButtonModule],
+ standalone: true,
+ template: `
+
+ `,
+})
+export class ButtonComponent {
+ @Input() color: 'primary' | 'accent' | 'warn' = 'primary';
+}
diff --git a/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/input.component.ts.template b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/input.component.ts.template
new file mode 100644
index 0000000..59f4b2e
--- /dev/null
+++ b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/input.component.ts.template
@@ -0,0 +1,32 @@
+import { Component } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatInputModule } from '@angular/material/input';
+import { MatIconModule } from '@angular/material/icon';
+import { CommonModule } from '@angular/common';
+
+@Component({
+ selector: 'input-clearable-example',
+ template: `
+
+ Clearable input
+
+
+
+ `,
+ standalone: true,
+ providers: [],
+ imports: [
+ MatFormFieldModule,
+ MatInputModule,
+ FormsModule,
+ ReactiveFormsModule,
+ MatIconModule,
+ CommonModule,
+ ],
+})
+export class InputComponent {
+ value = 'Clear me';
+}
diff --git a/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/slider.component.ts.template b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/slider.component.ts.template
new file mode 100644
index 0000000..b6c920a
--- /dev/null
+++ b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/slider.component.ts.template
@@ -0,0 +1,50 @@
+import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
+import { MatSliderModule } from '@angular/material/slider';
+import type { QwikifiedComponentProps, WithRequiredProps } from '@qwikdev/qwik-angular';
+
+type SliderComponentInputs = 'min' | 'max' | 'step' | 'sliderValue' | 'thumbLabel';
+
+type SliderComponentOutputs = 'sliderValueChanged';
+
+type RequiredPropValues = 'sliderValue';
+
+// using utility types to assemble a type object for qwikified SliderComponent
+// that has all inputs and typed output handlers of Angular SliderComponent
+type OptionalSliderComponentProps = QwikifiedComponentProps<
+ SliderComponent,
+ SliderComponentInputs,
+ SliderComponentOutputs
+>;
+
+// also marking "sliderValue" as required and exporting final type
+export type SliderComponentProps = WithRequiredProps<
+ OptionalSliderComponentProps,
+ RequiredPropValues
+>;
+
+@Component({
+ selector: 'app-slider',
+ imports: [MatSliderModule, FormsModule, ReactiveFormsModule],
+ standalone: true,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+ `,
+})
+export class SliderComponent {
+ @Input() min = 0;
+ @Input() max = 100;
+ @Input() step = 5;
+ @Input() sliderValue = 20;
+ @Input() thumbLabel = true;
+
+ @Output() readonly sliderValueChanged = new EventEmitter();
+
+ onSliderValueChange(value: number) {
+ this.sliderValueChanged.emit(value);
+ this.sliderValue = value;
+ }
+}
diff --git a/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.html.template b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.html.template
new file mode 100644
index 0000000..fb5cbc8
--- /dev/null
+++ b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.html.template
@@ -0,0 +1,47 @@
+
+
+ Filter
+
+
+
+
+
+
+
+ ID |
+ {{ row.id }} |
+
+
+
+
+ Progress |
+ {{ row.progress }}% |
+
+
+
+
+ Name |
+ {{ row.name }} |
+
+
+
+
+ Fruit |
+ {{ row.fruit }} |
+
+
+
+
+
+
+
+ No data matching the filter "{{ input.value }}" |
+
+
+
+
+
+
diff --git a/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.scss.template b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.scss.template
new file mode 100644
index 0000000..ceefc1b
--- /dev/null
+++ b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.scss.template
@@ -0,0 +1,17 @@
+.table-container {
+ padding: 30px 70px;
+}
+
+table {
+ width: 100%;
+}
+
+.mat-mdc-form-field {
+ font-size: 14px;
+ width: 100%;
+}
+
+td,
+th {
+ width: 25%;
+}
diff --git a/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.ts.template b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.ts.template
new file mode 100644
index 0000000..1ee39d5
--- /dev/null
+++ b/packages/qwik-nx/src/utils/angular/files/material/integration-files/components/table/table.component.ts.template
@@ -0,0 +1,51 @@
+import { type AfterViewInit, Component, ViewChild, Input } from '@angular/core';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
+import { MatSort, MatSortModule } from '@angular/material/sort';
+import { MatTableDataSource, MatTableModule } from '@angular/material/table';
+import { MatInputModule } from '@angular/material/input';
+import type { QwikifiedComponentProps } from '@qwikdev/qwik-angular';
+
+export interface TableUserData {
+ id: string;
+ name: string;
+ progress: string;
+ fruit: string;
+}
+
+type TableComponentInputs = 'users';
+export type TableComponentProps = QwikifiedComponentProps;
+
+@Component({
+ selector: 'app-table-component',
+ styleUrls: ['table.component.scss'],
+ templateUrl: 'table.component.html',
+ standalone: true,
+ imports: [MatTableModule, MatSortModule, MatPaginatorModule, MatFormFieldModule, MatInputModule],
+})
+export class TableComponent implements AfterViewInit {
+ displayedColumns: string[] = ['id', 'name', 'progress', 'fruit'];
+ dataSource = new MatTableDataSource();
+
+ @ViewChild(MatPaginator) paginator!: MatPaginator;
+ @ViewChild(MatSort) sort!: MatSort;
+
+ @Input()
+ set users(users: TableUserData[]) {
+ this.dataSource = new MatTableDataSource(users);
+ }
+
+ ngAfterViewInit() {
+ this.dataSource.paginator = this.paginator;
+ this.dataSource.sort = this.sort;
+ }
+
+ applyFilter(event: Event) {
+ const filterValue = (event.target as HTMLInputElement).value;
+ this.dataSource.filter = filterValue.trim().toLowerCase();
+
+ if (this.dataSource.paginator) {
+ this.dataSource.paginator.firstPage();
+ }
+ }
+}
diff --git a/packages/qwik-nx/src/utils/angular/files/material/integration-files/index.ts.template b/packages/qwik-nx/src/utils/angular/files/material/integration-files/index.ts.template
new file mode 100644
index 0000000..e618b21
--- /dev/null
+++ b/packages/qwik-nx/src/utils/angular/files/material/integration-files/index.ts.template
@@ -0,0 +1,16 @@
+import { qwikify$ } from '@qwikdev/qwik-angular';
+import { type SliderComponentProps, SliderComponent } from './components//slider.component';
+import { type ButtonComponentProps, ButtonComponent } from './components/button.component';
+import {
+ TableComponent,
+ type TableUserData,
+ type TableComponentProps,
+} from './components/table/table.component';
+
+export const MaterialSlider = qwikify$(SliderComponent, {
+ eagerness: 'hover',
+});
+export const MaterialButton = qwikify$(ButtonComponent);
+export const MaterialTable = qwikify$(TableComponent);
+
+export { ButtonComponentProps, SliderComponentProps, TableUserData, TableComponentProps };
diff --git a/packages/qwik-nx/src/utils/angular/files/material/styles/styles.scss.template b/packages/qwik-nx/src/utils/angular/files/material/styles/styles.scss.template
new file mode 100644
index 0000000..2a30aff
--- /dev/null
+++ b/packages/qwik-nx/src/utils/angular/files/material/styles/styles.scss.template
@@ -0,0 +1,36 @@
+// Custom Theming for Angular Material
+// For more information: https://material.angular.io/guide/theming
+@use '@angular/material' as mat;
+// Plus imports for other components in your app.
+
+// Include the common styles for Angular Material. We include this here so that you only
+// have to load a single css file for Angular Material in your app.
+// Be sure that you only ever include this mixin once!
+@include mat.core();
+
+// Define the palettes for your theme using the Material Design palettes available in palette.scss
+// (imported above). For each palette, you can optionally specify a default, lighter, and darker
+// hue. Available color palettes: https://material.io/design/color/
+$theme-primary: mat.define-palette(mat.$indigo-palette);
+$theme-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
+
+// The warn palette is optional (defaults to red).
+$theme-warn: mat.define-palette(mat.$red-palette);
+
+// Create the theme object. A theme consists of configurations for individual
+// theming systems such as "color" or "typography".
+$theme: mat.define-light-theme(
+ (
+ color: (
+ primary: $theme-primary,
+ accent: $theme-accent,
+ warn: $theme-warn,
+ ),
+ typography: mat.define-typography-config(),
+ )
+);
+
+// Include theme styles for core and each component used in your app.
+// Alternatively, you can import and @include the theme mixins for each component
+// that you are using.
+@include mat.all-component-themes($theme);
\ No newline at end of file
diff --git a/packages/qwik-nx/src/utils/angular/init.ts b/packages/qwik-nx/src/utils/angular/init.ts
new file mode 100644
index 0000000..fa5fe77
--- /dev/null
+++ b/packages/qwik-nx/src/utils/angular/init.ts
@@ -0,0 +1,198 @@
+import {
+ GeneratorCallback,
+ Tree,
+ addDependenciesToPackageJson,
+ generateFiles,
+ joinPathFragments,
+ output,
+ readJson,
+ writeJson,
+} from '@nx/devkit';
+import path = require('path');
+import {
+ angularVersion,
+ qwikAngularVersion,
+ vitePluginAngularVersion,
+} from '../versions';
+import { updateViteConfig } from '../update-vite-config';
+import { normalizeViteConfigFilePathWithTree } from '@nx/vite';
+
+export interface AngularInitSchema {
+ demoFilePath: string;
+ installMaterialExample: boolean;
+ projectRoot: string;
+ isApp: boolean;
+}
+
+function addFiles(tree: Tree, options: AngularInitSchema) {
+ generateFiles(
+ tree,
+ path.join(
+ __dirname,
+ 'files',
+ options.installMaterialExample ? 'material/integration-files' : 'demo'
+ ),
+ options.demoFilePath,
+ {}
+ );
+
+ if (options.installMaterialExample && options.isApp) {
+ const added = addRootStyles(tree, options);
+ if (added) {
+ generateFiles(
+ tree,
+ path.join(__dirname, 'files/material/styles'),
+ joinPathFragments(options.projectRoot, 'src'),
+ {}
+ );
+ } else {
+ output.warn({
+ title: 'Failed to add material theme',
+ bodyLines: [
+ "Your integration is still functional, however you'll need to add Angular Material theme manually",
+ // TODO: link to the docs
+ ],
+ });
+ }
+ }
+}
+
+function addRootStyles(tree: Tree, options: AngularInitSchema): boolean {
+ const rootTsxPath = joinPathFragments(options.projectRoot, 'src/root.tsx');
+ const importStatement = `import './theme.scss';`;
+
+ if (tree.exists(rootTsxPath)) {
+ let rootTsxContent = tree.read(rootTsxPath, 'utf-8')!;
+ const indexToInsert = rootTsxContent.indexOf(`import './global.css';`);
+ if (indexToInsert !== -1) {
+ const before = rootTsxContent.slice(0, indexToInsert);
+ const after = rootTsxContent.slice(indexToInsert);
+ rootTsxContent = `${before}${importStatement}\n${after}`;
+ } else {
+ rootTsxContent = `${importStatement}\n${rootTsxContent}`;
+ }
+ tree.write(rootTsxPath, rootTsxContent);
+ return true;
+ }
+
+ return false;
+}
+
+function addDependencies(
+ tree: Tree,
+ installMaterial: boolean
+): GeneratorCallback {
+ const devDependencies = {
+ '@angular/cdk': angularVersion,
+ '@angular/common': angularVersion,
+ '@analogjs/vite-plugin-angular': vitePluginAngularVersion,
+ '@qwikdev/qwik-angular': qwikAngularVersion,
+ '@angular-devkit/build-angular': angularVersion,
+ '@angular/compiler': angularVersion,
+ '@angular/compiler-cli': angularVersion,
+ '@angular/core': angularVersion,
+ '@angular/forms': angularVersion,
+ '@angular/platform-browser-dynamic': angularVersion,
+ '@angular/platform-browser': angularVersion,
+ '@angular/platform-server': angularVersion,
+ '@ngtools/webpack': angularVersion,
+ };
+ if (installMaterial) {
+ Object.assign(devDependencies, {
+ '@angular/material': angularVersion,
+ });
+ }
+ return addDependenciesToPackageJson(tree, {}, devDependencies);
+}
+
+export function addAngularPluginToViteConfig(
+ tree: Tree,
+ options: AngularInitSchema
+) {
+ const viteConfigPath = normalizeViteConfigFilePathWithTree(
+ tree,
+ options.projectRoot
+ );
+
+ if (!viteConfigPath) {
+ throw new Error(`Could not resolve vite config at ${options.projectRoot}`);
+ }
+ const viteConfig = tree.read(viteConfigPath)!.toString();
+ const bundleSassFilesInDevMode = `bundleSassFilesInDevMode: {
+ paths: ["src/theme.scss"],
+ compileOptions: { loadPaths: ["node_modules"] },
+ }`;
+ const updatedViteConfig = updateViteConfig(viteConfig, {
+ imports: [
+ {
+ namedImports: ['angular'],
+ importPath: '@qwikdev/qwik-angular/vite',
+ },
+ ],
+ vitePlugins: [
+ `angular({
+ tsconfig: "${
+ getAppTsConfigFileName(tree, options) ??
+ ''
+ }",
+ componentsDir: "integrations/angular/components",
+ ${options.installMaterialExample ? bundleSassFilesInDevMode : ''}
+ })`,
+ ],
+ });
+ tree.write(viteConfigPath, updatedViteConfig);
+}
+
+function getAppTsConfigFileName(
+ tree: Tree,
+ options: AngularInitSchema
+): string | null {
+ for (const tsConfigName of ['tsconfig.app.json', 'tsconfig.json']) {
+ const tsConfigPath = joinPathFragments(options.projectRoot, tsConfigName);
+ if (tree.exists(tsConfigPath)) {
+ return tsConfigName;
+ }
+ }
+
+ output.warn({
+ title: `Could not resolve tsconfig at ${options.projectRoot}`,
+ bodyLines: [
+ 'This means the Angular functionality may appear broken. In order to fix it, please add "noEmit": false" manually',
+ ],
+ });
+ return null;
+}
+
+function updateTsConfig(tree: Tree, options: AngularInitSchema) {
+ const tsConfigFileName = getAppTsConfigFileName(tree, options);
+ if (!tsConfigFileName) {
+ return;
+ }
+ const tsConfig = readJson(
+ tree,
+ joinPathFragments(options.projectRoot, tsConfigFileName)
+ );
+ tsConfig.compilerOptions.noEmit = false;
+ tsConfig.compilerOptions.experimentalDecorators = true;
+ tsConfig.angularCompilerOptions = {
+ enableI18nLegacyMessageIdFormat: false,
+ strictInjectionParameters: true,
+ strictInputAccessModifiers: true,
+ strictTemplates: true,
+ };
+ writeJson(tree, tsConfigFileName, tsConfig);
+}
+
+/**
+ * - adds angular example component (either Material or plain angular one)
+ * - installs necessary dependencies
+ */
+export function angularInit(
+ tree: Tree,
+ options: AngularInitSchema
+): GeneratorCallback {
+ addFiles(tree, options);
+ addAngularPluginToViteConfig(tree, options);
+ updateTsConfig(tree, options);
+ return addDependencies(tree, !!options.installMaterialExample);
+}
diff --git a/packages/qwik-nx/src/utils/versions.ts b/packages/qwik-nx/src/utils/versions.ts
index af970bf..6b8da4f 100644
--- a/packages/qwik-nx/src/utils/versions.ts
+++ b/packages/qwik-nx/src/utils/versions.ts
@@ -24,14 +24,14 @@ export const nxKitVersion = '^3.0.2';
export const wranglerVersion = '^3.1.0';
export const nxCloudflareWrangler = '^2.4.2';
-// netlify integraiton
+// netlify integration
export const netlifyCliVersion = '^15.5.0';
// storybook
export const storybookFrameworkQwikVersion = '^0.2.0';
export const typesMdx = '^2.0.3';
-// react integartion
+// react integration
export const qwikReactVersion = '^0.5.0';
export const reactVersion = '^18.0.0';
export const reactDOMVersion = '^18.0.0';
@@ -42,6 +42,11 @@ export const emotionStyledVersion = '^11.10.0';
export const muiMaterialVersion = '^5.12.0';
export const muiDataGridVersion = '^6.2.0';
+// angular integration
+export const angularVersion = '^16.0.0';
+export const vitePluginAngularVersion = '~0.2.0';
+export const qwikAngularVersion = '~0.1.0';
+
// other
export const eslintVersion = '~8.36.0';
export const tsEslintVersion = '~5.43.0';