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';