Skip to main content

JavaScript Front-End

A sophisticated monorepo archetype built with NX that supports multiple frontend frameworks (React, Angular, Vue.js) in a single workspace, providing unified tooling, shared libraries, and streamlined development workflows for enterprise-scale frontend development.

Overview

This archetype generates a comprehensive NX-based monorepo workspace designed to house multiple frontend applications and shared libraries. It provides a scalable architecture for teams working on diverse frontend technologies while maintaining consistency, code sharing, and efficient build processes.

Technology Stack

  • NX: Modern build system and monorepo toolkit
  • PNPM: Fast, disk space efficient package manager
  • React 18: Modern React applications with TypeScript
  • Angular 17+: Latest Angular applications with TypeScript
  • Vue.js 3: Vue applications with Composition API and TypeScript
  • TypeScript 5+: Strong typing across all applications
  • Jest: Unit testing framework
  • Cypress: End-to-end testing
  • ESLint: Code linting with framework-specific rules
  • Prettier: Code formatting and consistency
  • Storybook: Component documentation and testing

Key Features

Monorepo Architecture

  • Multiple Applications: Support for React, Angular, and Vue.js apps
  • Shared Libraries: Common utilities, components, and business logic
  • Code Generation: NX generators for consistent project structure
  • Dependency Graph: Visual dependency management and analysis
  • Incremental Builds: Only build what changed

NX Features

  • Computation Caching: Distributed and local caching for faster builds
  • Task Orchestration: Parallel execution of tasks across projects
  • Code Scaffolding: Generators for apps, libraries, and components
  • Workspace Analysis: Dependency tracking and impact analysis
  • Plugin Ecosystem: Rich ecosystem of NX plugins

Development Experience

  • Hot Module Replacement: Fast development across all frameworks
  • Unified Tooling: Consistent development experience
  • Shared Configuration: Common ESLint, Prettier, and TypeScript configs
  • Cross-Framework Sharing: Share code between different frameworks
  • Development Server: Unified development server management

Project Structure

frontend-workspace/
├── apps/ # Applications
│ ├── web-app/ # React application
│ │ ├── src/
│ │ │ ├── app/
│ │ │ ├── components/
│ │ │ ├── pages/
│ │ │ ├── hooks/
│ │ │ ├── services/
│ │ │ ├── types/
│ │ │ ├── utils/
│ │ │ ├── main.tsx
│ │ │ └── index.html
│ │ ├── project.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.app.json
│ │ └── vite.config.ts
│ ├── admin-app/ # Angular application
│ │ ├── src/
│ │ │ ├── app/
│ │ │ ├── assets/
│ │ │ ├── environments/
│ │ │ ├── main.ts
│ │ │ ├── index.html
│ │ │ └── styles.css
│ │ ├── project.json
│ │ ├── tsconfig.json
│ │ ├── tsconfig.app.json
│ │ └── angular.json
│ ├── mobile-app/ # Vue.js application
│ │ ├── src/
│ │ │ ├── components/
│ │ │ ├── views/
│ │ │ ├── composables/
│ │ │ ├── stores/
│ │ │ ├── router/
│ │ │ ├── types/
│ │ │ ├── main.ts
│ │ │ └── App.vue
│ │ ├── project.json
│ │ ├── tsconfig.json
│ │ ├── vite.config.ts
│ │ └── index.html
│ └── storybook/ # Storybook application
│ ├── .storybook/
│ ├── src/
│ └── project.json
├── libs/ # Shared libraries
│ ├── shared/ # Shared utilities and types
│ │ ├── ui/ # Framework-agnostic UI components
│ │ │ ├── src/
│ │ │ │ ├── lib/
│ │ │ │ │ ├── components/
│ │ │ │ │ ├── tokens/
│ │ │ │ │ ├── themes/
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ ├── project.json
│ │ │ ├── tsconfig.json
│ │ │ ├── tsconfig.lib.json
│ │ │ └── vite.config.ts
│ │ ├── utils/ # Common utilities
│ │ │ ├── src/
│ │ │ │ ├── lib/
│ │ │ │ │ ├── validation/
│ │ │ │ │ ├── formatters/
│ │ │ │ │ ├── helpers/
│ │ │ │ │ ├── constants/
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ ├── project.json
│ │ │ └── tsconfig.json
│ │ ├── types/ # Shared TypeScript types
│ │ │ ├── src/
│ │ │ │ ├── lib/
│ │ │ │ │ ├── api/
│ │ │ │ │ ├── domain/
│ │ │ │ │ ├── ui/
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ ├── project.json
│ │ │ └── tsconfig.json
│ │ └── api/ # API client library
│ │ ├── src/
│ │ │ ├── lib/
│ │ │ │ ├── clients/
│ │ │ │ ├── services/
│ │ │ │ ├── interceptors/
│ │ │ │ ├── types/
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── project.json
│ │ └── tsconfig.json
│ ├── react/ # React-specific libraries
│ │ ├── ui-components/ # React UI component library
│ │ │ ├── src/
│ │ │ │ ├── lib/
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── Button/
│ │ │ │ │ │ ├── Input/
│ │ │ │ │ │ ├── Modal/
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── hooks/
│ │ │ │ │ └── index.ts
│ │ │ │ └── index.ts
│ │ │ ├── project.json
│ │ │ ├── tsconfig.json
│ │ │ └── vite.config.ts
│ │ └── hooks/ # React custom hooks
│ │ ├── src/
│ │ │ ├── lib/
│ │ │ │ ├── useApi/
│ │ │ │ ├── useAuth/
│ │ │ │ ├── useLocalStorage/
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── project.json
│ │ └── tsconfig.json
│ ├── angular/ # Angular-specific libraries
│ │ ├── ui-components/ # Angular UI component library
│ │ │ ├── src/
│ │ │ │ ├── lib/
│ │ │ │ │ ├── components/
│ │ │ │ │ ├── services/
│ │ │ │ │ ├── directives/
│ │ │ │ │ ├── pipes/
│ │ │ │ │ └── public-api.ts
│ │ │ │ └── index.ts
│ │ │ ├── project.json
│ │ │ ├── tsconfig.json
│ │ │ └── ng-package.json
│ │ └── services/ # Angular services
│ │ ├── src/
│ │ │ ├── lib/
│ │ │ │ ├── auth/
│ │ │ │ ├── http/
│ │ │ │ ├── state/
│ │ │ │ └── public-api.ts
│ │ │ └── index.ts
│ │ ├── project.json
│ │ └── tsconfig.json
│ └── vue/ # Vue-specific libraries
│ ├── ui-components/ # Vue UI component library
│ │ ├── src/
│ │ │ ├── lib/
│ │ │ │ ├── components/
│ │ │ │ ├── composables/
│ │ │ │ ├── directives/
│ │ │ │ └── index.ts
│ │ │ └── index.ts
│ │ ├── project.json
│ │ ├── tsconfig.json
│ │ └── vite.config.ts
│ └── composables/ # Vue composables
│ ├── src/
│ │ ├── lib/
│ │ │ ├── useApi/
│ │ │ ├── useAuth/
│ │ │ ├── useValidation/
│ │ │ └── index.ts
│ │ └── index.ts
│ ├── project.json
│ └── tsconfig.json
├── tools/ # Build tools and scripts
│ ├── eslint-rules/ # Custom ESLint rules
│ ├── webpack-plugins/ # Custom Webpack plugins
│ └── generators/ # Custom NX generators
├── docs/ # Documentation
│ ├── architecture.md
│ ├── development.md
│ └── deployment.md
├── .github/ # GitHub Actions workflows
│ └── workflows/
│ ├── ci.yml
│ ├── deploy.yml
│ └── release.yml
├── nx.json # NX workspace configuration
├── package.json # Root package.json
├── pnpm-workspace.yaml # PNPM workspace configuration
├── tsconfig.base.json # Base TypeScript configuration
├── .eslintrc.json # ESLint configuration
├── .prettierrc # Prettier configuration
├── jest.config.ts # Jest configuration
└── README.md # Project documentation

NX Configuration

Workspace Configuration

// nx.json
{
"npmScope": "frontend-workspace",
"affected": {
"defaultBase": "main"
},
"cli": {
"packageManager": "pnpm",
"analytics": false
},
"defaultProject": "web-app",
"generators": {
"@nx/react": {
"application": {
"style": "css",
"linter": "eslint",
"bundler": "vite",
"unitTestRunner": "jest",
"e2eTestRunner": "cypress"
},
"component": {
"style": "css"
},
"library": {
"style": "css",
"linter": "eslint",
"unitTestRunner": "jest"
}
},
"@nx/angular": {
"application": {
"style": "scss",
"linter": "eslint",
"unitTestRunner": "jest",
"e2eTestRunner": "cypress"
},
"component": {
"style": "scss"
},
"library": {
"style": "scss",
"linter": "eslint",
"unitTestRunner": "jest"
}
},
"@nx/vue": {
"application": {
"style": "css",
"linter": "eslint",
"unitTestRunner": "vitest",
"e2eTestRunner": "cypress"
}
}
},
"namedInputs": {
"default": ["{projectRoot}/**/*", "sharedGlobals"],
"production": [
"default",
"!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)",
"!{projectRoot}/tsconfig.spec.json",
"!{projectRoot}/jest.config.[jt]s",
"!{projectRoot}/.eslintrc.json",
"!{projectRoot}/src/test-setup.[jt]s",
"!{projectRoot}/test-setup.[jt]s"
],
"sharedGlobals": []
},
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"],
"cache": true
},
"test": {
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
"cache": true
},
"e2e": {
"inputs": ["default", "^production"],
"cache": true
},
"lint": {
"inputs": ["default", "{workspaceRoot}/.eslintrc.json"],
"cache": true
}
},
"plugins": [
{
"plugin": "@nx/vite/plugin",
"options": {
"buildTargetName": "build",
"testTargetName": "test",
"serveTargetName": "serve",
"previewTargetName": "preview",
"serveStaticTargetName": "serve-static"
}
},
{
"plugin": "@nx/eslint/plugin",
"options": {
"targetName": "lint"
}
},
{
"plugin": "@nx/jest/plugin",
"options": {
"targetName": "test"
}
},
{
"plugin": "@nx/cypress/plugin",
"options": {
"targetName": "e2e",
"openTargetName": "open-cypress",
"componentTestingTargetName": "component-test"
}
}
]
}

Shared Libraries

Shared UI Component Library

// libs/shared/ui/src/lib/components/Button/Button.ts
export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
onClick?: () => void
children: React.ReactNode | string
className?: string
type?: 'button' | 'submit' | 'reset'
}

// React implementation
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
onClick,
children,
className = '',
type = 'button'
}) => {
const baseClasses = 'inline-flex items-center justify-center font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors'

const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
outline: 'border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-blue-500',
ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500'
}

const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base'
}

const classes = `${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className}`

return (
<button
type={type}
className={classes}
disabled={disabled || loading}
onClick={onClick}
>
{loading && (
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
)}
{children}
</button>
)
}

// Angular implementation
// libs/angular/ui-components/src/lib/components/button/button.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core'

@Component({
selector: 'ui-button',
template: `
<button
[type]="type"
[class]="getClasses()"
[disabled]="disabled || loading"
(click)="onClick.emit()"
>
<svg *ngIf="loading" class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<ng-content></ng-content>
</button>
`
})
export class ButtonComponent {
@Input() variant: 'primary' | 'secondary' | 'outline' | 'ghost' = 'primary'
@Input() size: 'sm' | 'md' | 'lg' = 'md'
@Input() disabled = false
@Input() loading = false
@Input() type: 'button' | 'submit' | 'reset' = 'button'
@Input() className = ''

@Output() onClick = new EventEmitter<void>()

getClasses(): string {
const baseClasses = 'inline-flex items-center justify-center font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors'

const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
outline: 'border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-blue-500',
ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500'
}

const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base'
}

return `${baseClasses} ${variantClasses[this.variant]} ${sizeClasses[this.size]} ${this.className}`
}
}

// Vue implementation
// libs/vue/ui-components/src/lib/components/Button.vue
<template>
<button
:type="type"
:class="classes"
:disabled="disabled || loading"
@click="$emit('click')"
>
<svg v-if="loading" class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<slot />
</button>
</template>

<script setup lang="ts">
interface Props {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost'
size?: 'sm' | 'md' | 'lg'
disabled?: boolean
loading?: boolean
type?: 'button' | 'submit' | 'reset'
className?: string
}

const props = withDefaults(defineProps<Props>(), {
variant: 'primary',
size: 'md',
disabled: false,
loading: false,
type: 'button',
className: ''
})

defineEmits<{
click: []
}>()

const classes = computed(() => {
const baseClasses = 'inline-flex items-center justify-center font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors'

const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-600 text-white hover:bg-gray-700 focus:ring-gray-500',
outline: 'border border-gray-300 text-gray-700 bg-white hover:bg-gray-50 focus:ring-blue-500',
ghost: 'text-gray-700 hover:bg-gray-100 focus:ring-gray-500'
}

const sizeClasses = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base'
}

return `${baseClasses} ${variantClasses[props.variant]} ${sizeClasses[props.size]} ${props.className}`
})
</script>

Shared API Client

// libs/shared/api/src/lib/clients/BaseApiClient.ts
export class BaseApiClient {
private baseURL: string
private defaultHeaders: Record<string, string>

constructor(baseURL: string, defaultHeaders: Record<string, string> = {}) {
this.baseURL = baseURL
this.defaultHeaders = {
'Content-Type': 'application/json',
...defaultHeaders
}
}

private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseURL}${endpoint}`

const config: RequestInit = {
headers: {
...this.defaultHeaders,
...options.headers
},
...options
}

try {
const response = await fetch(url, config)

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}

return await response.json()
} catch (error) {
console.error('API request failed:', error)
throw error
}
}

async get<T>(endpoint: string, headers?: Record<string, string>): Promise<T> {
return this.request<T>(endpoint, { method: 'GET', headers })
}

async post<T>(endpoint: string, data?: unknown, headers?: Record<string, string>): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
headers
})
}

async put<T>(endpoint: string, data?: unknown, headers?: Record<string, string>): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
headers
})
}

async delete<T>(endpoint: string, headers?: Record<string, string>): Promise<T> {
return this.request<T>(endpoint, { method: 'DELETE', headers })
}

setAuthToken(token: string): void {
this.defaultHeaders.Authorization = `Bearer ${token}`
}

removeAuthToken(): void {
delete this.defaultHeaders.Authorization
}
}

// libs/shared/api/src/lib/services/UserService.ts
import { BaseApiClient } from '../clients/BaseApiClient'
import type { User, CreateUserRequest, UpdateUserRequest } from '@frontend-workspace/shared/types'

export class UserService extends BaseApiClient {
async getUsers(): Promise<User[]> {
const response = await this.get<{ data: User[] }>('/users')
return response.data
}

async getUserById(id: string): Promise<User> {
const response = await this.get<{ data: User }>(`/users/${id}`)
return response.data
}

async createUser(userData: CreateUserRequest): Promise<User> {
const response = await this.post<{ data: User }>('/users', userData)
return response.data
}

async updateUser(id: string, userData: UpdateUserRequest): Promise<User> {
const response = await this.put<{ data: User }>(`/users/${id}`, userData)
return response.data
}

async deleteUser(id: string): Promise<void> {
await this.delete(`/users/${id}`)
}
}

Application Examples

React Application

// apps/web-app/src/app/App.tsx
import React from 'react'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import { Button } from '@frontend-workspace/react/ui-components'
import { useAuth } from '@frontend-workspace/react/hooks'
import { HomePage } from './pages/HomePage'
import { DashboardPage } from './pages/DashboardPage'
import { LoginPage } from './pages/LoginPage'

export function App() {
const { isAuthenticated, user } = useAuth()

return (
<Router>
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-semibold">React App</h1>
</div>
<div className="flex items-center space-x-4">
{isAuthenticated ? (
<>
<span>Welcome, {user?.name}</span>
<Button variant="outline" onClick={() => console.log('Logout')}>
Logout
</Button>
</>
) : (
<Button onClick={() => console.log('Login')}>
Login
</Button>
)}
</div>
</div>
</div>
</header>

<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/login" element={<LoginPage />} />
</Routes>
</main>
</div>
</Router>
)
}

export default App

Angular Application

// apps/admin-app/src/app/app.component.ts
import { Component } from '@angular/core'
import { AuthService } from '@frontend-workspace/angular/services'

@Component({
selector: 'app-root',
template: `
<div class="min-h-screen bg-gray-50">
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<h1 class="text-xl font-semibold">Admin App</h1>
</div>
<div class="flex items-center space-x-4">
<ng-container *ngIf="authService.isAuthenticated$ | async; else loginButton">
<span>Welcome, {{ (authService.user$ | async)?.name }}</span>
<ui-button variant="outline" (onClick)="logout()">
Logout
</ui-button>
</ng-container>
<ng-template #loginButton>
<ui-button (onClick)="login()">
Login
</ui-button>
</ng-template>
</div>
</div>
</div>
</header>

<main>
<router-outlet></router-outlet>
</main>
</div>
`
})
export class AppComponent {
constructor(public authService: AuthService) {}

login(): void {
console.log('Login')
}

logout(): void {
this.authService.logout()
}
}

Vue Application

<!-- apps/mobile-app/src/App.vue -->
<template>
<div class="min-h-screen bg-gray-50">
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<h1 class="text-xl font-semibold">Mobile App</h1>
</div>
<div class="flex items-center space-x-4">
<template v-if="isAuthenticated">
<span>Welcome, {{ user?.name }}</span>
<Button variant="outline" @click="logout">
Logout
</Button>
</template>
<template v-else>
<Button @click="login">
Login
</Button>
</template>
</div>
</div>
</div>
</header>

<main>
<RouterView />
</main>
</div>
</template>

<script setup lang="ts">
import { Button } from '@frontend-workspace/vue/ui-components'
import { useAuth } from '@frontend-workspace/vue/composables'

const { isAuthenticated, user, logout } = useAuth()

const login = () => {
console.log('Login')
}
</script>

Testing Strategy

Cross-Framework Component Testing

// libs/shared/ui/src/lib/components/Button/Button.test.ts
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'

describe('Button Component (React)', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})

it('calls onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button onClick={handleClick}>Click me</Button>)

fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})

it('is disabled when loading', () => {
render(<Button loading>Click me</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})

it('applies correct variant classes', () => {
render(<Button variant="outline">Click me</Button>)
const button = screen.getByRole('button')
expect(button).toHaveClass('border')
expect(button).toHaveClass('border-gray-300')
})
})

// libs/angular/ui-components/src/lib/components/button/button.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { ButtonComponent } from './button.component'

describe('ButtonComponent (Angular)', () => {
let component: ButtonComponent
let fixture: ComponentFixture<ButtonComponent>

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ButtonComponent]
}).compileComponents()

fixture = TestBed.createComponent(ButtonComponent)
component = fixture.componentInstance
})

it('should create', () => {
expect(component).toBeTruthy()
})

it('should emit onClick when clicked', () => {
spyOn(component.onClick, 'emit')

const button = fixture.nativeElement.querySelector('button')
button.click()

expect(component.onClick.emit).toHaveBeenCalled()
})

it('should be disabled when loading', () => {
component.loading = true
fixture.detectChanges()

const button = fixture.nativeElement.querySelector('button')
expect(button.disabled).toBeTruthy()
})
})

// libs/vue/ui-components/src/lib/components/Button.test.ts
import { mount } from '@vue/test-utils'
import { Button } from './Button.vue'

describe('Button Component (Vue)', () => {
it('renders with correct text', () => {
const wrapper = mount(Button, {
slots: {
default: 'Click me'
}
})

expect(wrapper.text()).toContain('Click me')
})

it('emits click event when clicked', async () => {
const wrapper = mount(Button)

await wrapper.find('button').trigger('click')

expect(wrapper.emitted('click')).toBeTruthy()
})

it('is disabled when loading', () => {
const wrapper = mount(Button, {
props: { loading: true }
})

expect(wrapper.find('button').element.disabled).toBe(true)
})
})

E2E Testing Across Applications

// apps/web-app-e2e/src/e2e/app.cy.ts
describe('React Web App', () => {
beforeEach(() => cy.visit('/'))

it('should display welcome message', () => {
cy.contains('Welcome to React App')
})

it('should navigate to dashboard when authenticated', () => {
cy.get('[data-cy="login-button"]').click()
cy.url().should('include', '/dashboard')
})
})

// apps/admin-app-e2e/src/e2e/app.cy.ts
describe('Angular Admin App', () => {
beforeEach(() => cy.visit('/'))

it('should display admin interface', () => {
cy.contains('Admin App')
})

it('should show user management section', () => {
cy.get('[data-cy="users-nav"]').click()
cy.url().should('include', '/users')
})
})

// apps/mobile-app-e2e/src/e2e/app.cy.ts
describe('Vue Mobile App', () => {
beforeEach(() => cy.visit('/'))

it('should display mobile interface', () => {
cy.contains('Mobile App')
})

it('should be responsive', () => {
cy.viewport('iphone-x')
cy.get('[data-cy="mobile-menu"]').should('be.visible')
})
})

NX Commands

Common Development Commands

# Start all applications
nx run-many --target=serve --projects=web-app,admin-app,mobile-app

# Build all applications
nx run-many --target=build --projects=web-app,admin-app,mobile-app

# Test all libraries
nx run-many --target=test --projects=shared-ui,shared-utils,shared-api

# Lint everything
nx run-many --target=lint --all

# Run affected projects only
nx affected --target=build
nx affected --target=test
nx affected --target=lint

# Generate new React app
nx g @nx/react:app new-react-app

# Generate new Angular app
nx g @nx/angular:app new-angular-app

# Generate new Vue app
nx g @nx/vue:app new-vue-app

# Generate shared library
nx g @nx/js:lib shared-feature

# Generate React component library
nx g @nx/react:lib react-components

# Generate Angular component library
nx g @nx/angular:lib angular-components

# Generate Vue component library
nx g @nx/vue:lib vue-components

# View dependency graph
nx graph

# Show project information
nx show project web-app

Quick Start

  1. Generate the workspace:

    archetect render git@github.com:p6m-archetypes/javascript-front-end.archetype.git
  2. Install dependencies:

    pnpm install
  3. Start development servers:

    # Start React app
    nx serve web-app

    # Start Angular app
    nx serve admin-app

    # Start Vue app
    nx serve mobile-app

    # Start Storybook
    nx serve storybook
  4. Access applications:

    React App: http://localhost:4200
    Angular App: http://localhost:4201
    Vue App: http://localhost:4202
    Storybook: http://localhost:4400
  5. Run tests:

    # All tests
    nx run-many --target=test --all

    # Specific project
    nx test web-app

    # E2E tests
    nx e2e web-app-e2e
  6. Build for production:

    nx run-many --target=build --projects=web-app,admin-app,mobile-app

Best Practices

Monorepo Organization

  • Keep shared code in libraries, not applications
  • Use clear naming conventions for projects
  • Implement proper dependency boundaries
  • Leverage NX affected commands for CI/CD

Code Sharing

  • Design framework-agnostic shared libraries
  • Use TypeScript for better type safety across projects
  • Implement consistent APIs across framework-specific libraries
  • Document shared components and utilities thoroughly

Development Workflow

  • Use feature branches for all changes
  • Run affected tests in CI/CD pipelines
  • Implement proper code review processes
  • Maintain consistent coding standards across frameworks

Performance

  • Leverage NX caching for faster builds
  • Use proper tree-shaking in all applications
  • Implement lazy loading where appropriate
  • Monitor bundle sizes across applications