diff --git a/.firebaserc b/.firebaserc
new file mode 100644
index 0000000..a5a06c0
--- /dev/null
+++ b/.firebaserc
@@ -0,0 +1,17 @@
+{
+ "projects": {
+ "default": "your-firebase-project-id"
+ },
+ "targets": {
+ "your-firebase-project-id": {
+ "hosting": {
+ "admin": [
+ "admin-site-id"
+ ],
+ "samples": [
+ "samples-site-id"
+ ]
+ }
+ }
+ }
+}
diff --git a/.github/workflows/PR_Check.yml b/.github/workflows/PR_Check.yml
index 5729f30..54b2672 100644
--- a/.github/workflows/PR_Check.yml
+++ b/.github/workflows/PR_Check.yml
@@ -2,8 +2,8 @@ name: PR Check
on:
pull_request:
- branches:
- - main
+ branches:
+ - main
jobs:
test:
@@ -16,12 +16,12 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v3
with:
- node-version: '18'
+ node-version: '22'
- name: Generate environment.ts from secrets
shell: bash
run: |
- cat > src/environments/environment.ts <<'EOF'
+ cat > environments/environment.ts <<'EOF'
export const environment = {
production: false,
supabaseUrl: '${{ secrets.SUPABASE_URL }}',
diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
index 7a1ebfc..dfa6a9e 100644
--- a/.github/workflows/e2e-tests.yml
+++ b/.github/workflows/e2e-tests.yml
@@ -40,8 +40,15 @@ jobs:
- run: |
echo "SUPABASE_ANON_KEY=$(supabase status -o env | grep SUPABASE_ANON_KEY | cut -d= -f2)" >> $GITHUB_ENV
+ - name: Start Angular web app
+ run: npm run start:local &
+ env: { NODE_ENV: "test" }
+
+ - name: Wait for Angular web app (≤30 s)
+ run: npx --yes wait-on http://localhost:4200 --timeout 30000
+
- run: npx playwright install --with-deps
- - run: npm run e2e:local
+ - run: npm run e2e:web:local
env: { CI: "true" }
- uses: actions/upload-artifact@v4
diff --git a/.github/workflows/google-cloudrun-docker.yml b/.github/workflows/google-cloudrun-docker.yml
index 6ecdf95..6d762a0 100644
--- a/.github/workflows/google-cloudrun-docker.yml
+++ b/.github/workflows/google-cloudrun-docker.yml
@@ -9,29 +9,31 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
- node-version: '18'
-
+ node-version: '20'
+ cache: npm
+
- run: npm ci
- - run: npm run test -- --browsers=ChromeHeadless --watch=false --no-progress
+ - run: npm run test:all -- --browsers=ChromeHeadless --watch=false --no-progress
deploy:
runs-on: ubuntu-latest
needs: test
steps:
- - uses: actions/checkout@v3
+ - uses: actions/checkout@v4
- - uses: actions/setup-node@v3
+ - uses: actions/setup-node@v4
with:
- node-version: '18'
+ node-version: '20'
+ cache: npm
- run: npm ci
- - run: npm run build --prod
+ - run: npm run build -- --configuration production
- uses: google-github-actions/auth@v2
with:
diff --git a/.gitignore b/.gitignore
index 3197472..fde308f 100755
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,7 @@ adminSdkConf.json
/test-results/
/playwright-report/
/playwright/.cache/
+
+#llms
+/llms/private
+/llms/priv
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..624f8cc
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,32 @@
+{
+ "semi": true,
+ "trailingComma": "es5",
+ "singleQuote": true,
+ "printWidth": 100,
+ "tabWidth": 2,
+ "useTabs": false,
+ "endOfLine": "lf",
+ "bracketSpacing": true,
+ "bracketSameLine": false,
+ "arrowParens": "avoid",
+ "overrides": [
+ {
+ "files": "*.html",
+ "options": {
+ "parser": "angular"
+ }
+ },
+ {
+ "files": "*.scss",
+ "options": {
+ "singleQuote": false
+ }
+ },
+ {
+ "files": "*.md",
+ "options": {
+ "proseWrap": "preserve"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..4d979e9
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,238 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## MOST IMPORTANT: Always read the LLM context files in `/llms/` before making any code changes or refactor proposals.
+
+## MOST IMPORTANT: NO WRITE COMMENTS in the codebase. Use PR comments instead.
+
+## Project Overview
+
+This is **angular.fun** - a modern Angular 20+ blog application with Supabase backend. The project is currently a monolithic structure in `/src` but is being refactored into a multi-project workspace with micro-frontends and shared libraries.
+
+### Current Architecture
+
+- **Main App**: `/src/app` - Monolithic structure containing both reader and admin functionality
+- **Reader**: `/src/app/reader` - Public blog with SSG/SSR capabilities
+- **Admin**: `/src/app/admin` - Admin panel with CSR for content management
+- **Shared**: `/src/app/shared` - Common components, services, models, and stores
+
+### Target Architecture (Multi-Project Workspace)
+
+- **projects/web**: Public blog with hybrid rendering (SSG for articles, SSR for home)
+- **projects/admin**: Pure CSR admin panel for Firebase Hosting
+- **projects/code-samples-mfe**: Micro-frontend for code samples via Native Federation
+- **projects/shared**: Shared library with UI components, patterns, data-access, and models
+
+## Common Development Commands
+
+### Development
+
+```bash
+npm start # Start main app (development mode)
+npm run start:local # Start with local Supabase configuration
+npm run start:local:docker # Open Docker for local development
+npm run start:local:backend # Start local Supabase instance
+```
+
+### Building
+
+```bash
+npm run build # Build for production
+npm run build:stats # Build with bundle analysis stats
+npm run analyze # Analyze bundle size with webpack-bundle-analyzer
+```
+
+### Testing
+
+```bash
+npm test # Run unit tests with Karma
+npm run e2e # Run E2E tests with Playwright
+npm run e2e:local # Run E2E tests against local environment
+```
+
+### Supabase Management
+
+```bash
+npx supabase start # Start local Supabase
+npx supabase stop # Stop local Supabase
+npm run schema:pull # Pull remote schema from cloud Supabase
+npm run db:createSeed # Create database seed file
+npm run db:seed # Initialize database with seed data
+```
+
+### Project-Specific Commands
+
+```bash
+ng serve admin # Serve admin project (when refactored)
+ng serve web # Serve web project (when refactored)
+ng build shared # Build shared library
+ng serve code-samples-mfe # Serve code samples micro-frontend
+```
+
+## Key Technologies & Stack
+
+- **Frontend**: Angular 20, NgRx Signals, Tailwind CSS, DaisyUI, Quill Editor, Highlight.js
+- **Backend**: Supabase (PostgreSQL, Authentication, Storage, Functions)
+- **Testing**: Playwright for E2E, Karma/Jasmine for unit tests
+- **Build**: Angular CLI with esbuild, SSR/SSG capabilities
+- **State Management**: NgRx SignalStore throughout all applications
+
+## Code Architecture Patterns
+
+### State Management
+
+- Use NgRx SignalStore for all state management
+- Feature-scoped stores in individual feature directories
+- Shared stores in `/src/app/shared/stores` -> refactor to `/projects/shared/src/data-access/stores` in new architecture
+
+### Component Structure
+
+- Standalone components using Angular's modern APIs
+- Feature-based organization with lazy-loaded routes
+- UI components are pure and stateless (inputs/outputs only)
+- Business logic encapsulated in services and stores
+
+### Data Access
+
+- **Public/Reader**: Uses PostgREST endpoints for SSG/SSR compatibility
+- **Admin**: Uses `@supabase/supabase-js` browser client
+- **Authentication**: Supabase Auth with role-based access control
+- **Storage**: Supabase Storage CDN for images and assets
+
+### Routing
+
+- Lazy-loaded feature modules
+- Route-level providers for feature isolation
+- Functional guards for authentication (`authAdminGuard`)
+
+## Development Guidelines
+
+### File Organization
+
+- The legacy monolith remains under `/src` until migration is complete — **do not modify** unless explicitly approved.
+- The target workspace lives under `/projects` and `/projects/shared`.
+
+**Apps**
+
+- `projects/web` — feature-first, standalone, lazy:
+ - `app/` (bootstrap, root routes)
+ - `features//` (routes, components, stores, services)
+ - `layout/` (shell, navigation, footer)
+ - `core/` (app-level providers: http, interceptors, guards)
+- `projects/admin` — CSR-only, same structure as `web`.
+- `projects/code-samples-mfe` — remote MFE, only required features and mfe routing.
+
+**Shared library**
+
+- `projects/shared/`
+ - `ui/` (pure presentational components; no business logic)
+ - `pattern/` (composable building blocks, form controls, table abstractions)
+ - `data-access/` (clients, repositories, query functions; PostgREST for web SSR/SSG, supabase-js for admin)
+ - `models/` (types, DTOs, schema definitions)
+ - `utils/` (pure functions, pipes, directives)
+
+**General rules**
+
+- Standalone + lazy everywhere; **no cross-feature TS imports**.
+- Promote reusable logic “upwards” (from feature → pattern/ui/data-access).
+- Keep guards/interceptors/providers at route-level or in `core/` per app.
+- Tailwind/DaisyUI configuration shared where possible; design tokens in `shared`.
+
+See the [architecture.txt](llms/private/architecture.txt) document for authoritative rules and dependency boundaries.
+
+### Styling
+
+- Use Tailwind CSS with DaisyUI components
+- Custom color palette: primary (#12372A), secondary (#436850), tertiary (#ADBC9F), quaternary (#FBFADA)
+- Monitor CSS bundle size with budget limits in angular.json
+
+### Type Safety
+
+- Strict TypeScript configuration enabled
+- Supabase types generated in `/src/app/types/supabase/` -> refactor to `/projects/shared/src/models/supabase/`
+- Use proper interfaces for all data models
+
+### Environment Configuration
+
+- `environment.ts` - Production
+- `environment.development.ts` - Development
+- `environment.local.ts` - Local Supabase instance
+
+## Supabase Integration
+
+### Supabase Configuration
+
+- projects/web uses PostgREST for SSG/SSR compatibility
+- rest uses `@supabase/supabase-js` for browser client
+
+### Database Schema
+
+- Posts, Comments, Tags, PostTags, Profiles tables
+- Row Level Security (RLS) policies for data protection
+- Migrations managed in `/supabase/migrations/`
+
+### Authentication
+
+- Email/password authentication
+- Role-based access (admin, user roles)
+- Protected admin routes with functional guards
+
+### Local Development
+
+- Use Docker for local Supabase instance
+- Access Supabase Studio at http://localhost:54323
+- Seed data available via scripts in `/scripts/`
+
+## Testing Strategy
+
+### E2E Tests (Playwright)
+
+- Tests in `/e2e/` directory
+- Configured for multiple browsers (Chromium, Firefox, WebKit)
+- Helper utilities in `/e2e/helpers/`
+- Local and CI configurations available
+
+### Unit Tests
+
+- Colocated spec files with components/services
+- Karma + Jasmine test runner -> refactor to use Vitest in the future
+- Focus on business logic and component behavior
+
+## Build & Deployment
+
+### Bundle Optimization
+
+- Lazy loading for all features
+- Bundle analysis with webpack-bundle-analyzer
+- Performance budgets enforced in angular.json
+- Tree-shaking enabled for optimal bundle sizes
+
+### Target Deployments
+
+- **Web**: Google Cloud Run (SSG/SSR)
+- **Admin**: Firebase Hosting (CSR)
+- **Code Samples MFE**: Firebase Hosting (MFE)
+- **Assets**: Supabase Storage CDN
+
+## Migration Status
+
+The project is currently migrating from monolithic structure to multi-project workspace:
+
+- Current: Single app in `/src`
+- Target: Separate projects in `/projects/` with shared libraries
+- Architecture guidance available in `/llms/private/architecture.txt`
+
+## LLM Context Files
+
+Always read these files before making refactor proposals or code edits:
+
+- [llms/**/architecture.txt](llms/private/architecture.txt)
+- [llms/**/llm-full.txt](llms/private/llm-full.txt)
+- [llms/**/app-description.txt](llms/private/app-description.txt)
+
+These documents are authoritative for:
+
+- Workspace and folder structure
+- Angular 20 style and coding conventions
+- Target apps, rendering modes, and deployment
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..2824339
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,35 @@
+# Dockerfile for Angular SSR (blog-ssg) on Cloud Run
+# Multi-stage: build Angular bundles, then run the SSR server.
+# Cloud Run expects the app to listen on PORT env var (default 8080).
+
+# ---------- Build stage ----------
+FROM node:20-bullseye AS builder
+WORKDIR /workspace
+
+# Install deps with pnpm (recommended for speed). Fallback to npm if needed.
+COPY package.json* pnpm-lock.yaml* package-lock.json* .npmrc* ./
+RUN corepack enable && corepack prepare pnpm@latest --activate || true
+RUN if [ -f pnpm-lock.yaml ]; then pnpm install --frozen-lockfile; elif [ -f package-lock.json ]; then npm ci; else pnpm install; fi
+
+COPY . .
+
+# Build browser, server (SSR). Prerender can be done in CI before building the image.
+RUN npx ng build blog-ssg --configuration=production
+RUN npx ng run blog-ssg:server:production
+# Optional:
+# RUN npx ng run blog-ssg:prerender --routes=apps/blog-ssg/routes.txt
+
+# ---------- Runtime stage ----------
+FROM node:20-bullseye-slim AS runtime
+ENV NODE_ENV=production
+WORKDIR /srv
+
+# Copy built artifacts
+COPY --from=builder /workspace/dist/apps/blog-ssg /srv/dist/apps/blog-ssg
+
+# Cloud Run provides PORT env. Angular SSR server must bind to this port.
+ENV PORT=8080
+EXPOSE 8080
+
+# Adjust path to the server entry if your project differs.
+CMD ["node", "dist/apps/blog-ssg/server/server.mjs"]
diff --git a/angular.json b/angular.json
index 1ad5041..0a8a2ee 100755
--- a/angular.json
+++ b/angular.json
@@ -3,49 +3,140 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
- "angularblogapp": {
+ "admin": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
- "style": "scss"
+ "style": "scss",
+ "changeDetection": "OnPush",
+ "displayBlock": true
}
},
- "root": "",
- "sourceRoot": "src",
- "prefix": "app",
+ "root": "projects/admin",
+ "sourceRoot": "projects/admin/src",
+ "prefix": "admin",
"architect": {
"build": {
- "builder": "@angular-devkit/build-angular:application",
+ "builder": "@angular/build:application",
"options": {
- "outputPath": "dist/angular-blog-app",
- "index": "src/index.html",
- "browser": "src/main.ts",
- "allowedCommonJsDependencies": [
- "highlight.js",
- "whatwg-url",
- "@supabase/node-fetch"
+ "browser": "projects/admin/src/main.ts",
+ "tsConfig": "projects/admin/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "projects/admin/public"
+ }
+ ],
+ "styles": [
+ "projects/admin/src/styles.scss"
+ ]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kB",
+ "maximumError": "1MB"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "4kB",
+ "maximumError": "8kB"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular/build:dev-server",
+ "configurations": {
+ "production": {
+ "buildTarget": "admin:build:production"
+ },
+ "development": {
+ "buildTarget": "admin:build:development"
+ }
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular/build:extract-i18n"
+ },
+ "test": {
+ "builder": "@angular/build:karma",
+ "options": {
+ "tsConfig": "projects/admin/tsconfig.spec.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "projects/admin/public"
+ }
+ ],
+ "styles": [
+ "projects/admin/src/styles.scss"
+ ]
+ }
+ },
+ "lint": {
+ "builder": "@angular-eslint/builder:lint",
+ "options": {
+ "lintFilePatterns": [
+ "projects/admin/**/*.ts",
+ "projects/admin/**/*.html"
],
+ "eslintConfig": "projects/admin/eslint.config.js"
+ }
+ }
+ }
+ },
+ "web": {
+ "projectType": "application",
+ "schematics": {
+ "@schematics/angular:component": {
+ "style": "scss",
+ "changeDetection": "OnPush",
+ "displayBlock": true
+ }
+ },
+ "root": "projects/web",
+ "sourceRoot": "projects/web/src",
+ "prefix": "web",
+ "architect": {
+ "build": {
+ "builder": "@angular/build:application",
+ "options": {
+ "browser": "projects/web/src/main.ts",
"polyfills": [
"zone.js"
],
- "tsConfig": "tsconfig.app.json",
+ "tsConfig": "projects/web/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
- "glob": "**/**",
- "input": "public"
+ "glob": "**/*",
+ "input": "projects/web/public"
}
],
"styles": [
- "src/styles.scss"
+ "projects/web/src/styles.scss"
],
- "scripts": [],
- "server": "src/main.server.ts",
+ "server": "projects/web/src/main.server.ts",
+ "outputMode": "server",
+ "ssr": {
+ "entry": "projects/web/src/server.ts"
+ },
"prerender": {
"routesFile": "routes.txt"
- },
- "ssr": {
- "entry": "server.ts"
}
},
"configurations": {
@@ -58,8 +149,8 @@
},
{
"type": "anyComponentStyle",
- "maximumWarning": "2kB",
- "maximumError": "4kB"
+ "maximumWarning": "4kB",
+ "maximumError": "8kB"
}
],
"outputHashing": "all"
@@ -67,79 +158,193 @@
"development": {
"optimization": false,
"extractLicenses": false,
- "sourceMap": true,
- "fileReplacements": [
- {
- "replace": "src/environments/environment.ts",
- "with": "src/environments/environment.development.ts"
- }
- ]
- },
- "local": {
- "optimization": false,
- "extractLicenses": false,
- "sourceMap": true,
- "fileReplacements": [
- {
- "replace": "src/environments/environment.ts",
- "with": "src/environments/environment.local.ts"
- }
- ]
+ "sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
- "builder": "@angular-devkit/build-angular:dev-server",
+ "builder": "@angular/build:dev-server",
"configurations": {
"production": {
- "buildTarget": "angularblogapp:build:production"
+ "buildTarget": "web:build:production"
},
"development": {
- "buildTarget": "angularblogapp:build:development"
+ "buildTarget": "web:build:development"
},
"local": {
- "buildTarget": "angularblogapp:build:local"
+ "buildTarget": "web:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
- "builder": "@angular-devkit/build-angular:extract-i18n"
+ "builder": "@angular/build:extract-i18n"
},
"test": {
- "builder": "@angular-devkit/build-angular:karma",
+ "builder": "@angular/build:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
- "tsConfig": "tsconfig.spec.json",
+ "tsConfig": "projects/web/tsconfig.spec.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
- "input": "public"
+ "input": "projects/web/public"
}
],
"styles": [
- "src/styles.scss"
+ "projects/web/src/styles.scss"
+ ]
+ }
+ },
+ "lint": {
+ "builder": "@angular-eslint/builder:lint",
+ "options": {
+ "lintFilePatterns": [
+ "projects/web/**/*.ts",
+ "projects/web/**/*.html"
],
- "scripts": []
+ "eslintConfig": "projects/web/eslint.config.js"
+ }
+ }
+ }
+ },
+ "shared": {
+ "projectType": "library",
+ "root": "projects/shared",
+ "sourceRoot": "projects/shared/src",
+ "prefix": "shared",
+ "architect": {
+ "build": {
+ "builder": "@angular/build:ng-packagr",
+ "configurations": {
+ "production": {
+ "tsConfig": "projects/shared/tsconfig.lib.prod.json"
+ },
+ "development": {
+ "tsConfig": "projects/shared/tsconfig.lib.json"
+ }
+ },
+ "defaultConfiguration": "production"
+ },
+ "test": {
+ "builder": "@angular/build:karma",
+ "options": {
+ "tsConfig": "projects/shared/tsconfig.spec.json",
+ "polyfills": [
+ "zone.js",
+ "zone.js/testing"
+ ]
}
},
- "e2e": {
- "builder": "playwright-ng-schematics:playwright",
+ "lint": {
+ "builder": "@angular-eslint/builder:lint",
+ "options": {
+ "lintFilePatterns": [
+ "projects/shared/**/*.ts",
+ "projects/shared/**/*.html"
+ ],
+ "eslintConfig": "projects/shared/eslint.config.js"
+ }
+ }
+ }
+ },
+ "code-samples-mfe": {
+ "projectType": "application",
+ "schematics": {
+ "@schematics/angular:component": {
+ "style": "scss",
+ "changeDetection": "OnPush",
+ "displayBlock": true
+ }
+ },
+ "root": "projects/code-samples-mfe",
+ "sourceRoot": "projects/code-samples-mfe/src",
+ "prefix": "mfe",
+ "architect": {
+ "build": {
+ "builder": "@angular/build:application",
"options": {
- "devServerTarget": "angularblogapp:serve"
+ "browser": "projects/code-samples-mfe/src/main.ts",
+ "tsConfig": "projects/code-samples-mfe/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "projects/code-samples-mfe/public"
+ }
+ ],
+ "styles": [
+ "projects/code-samples-mfe/src/styles.scss"
+ ]
+ },
+ "configurations": {
+ "production": {
+ "budgets": [
+ {
+ "type": "initial",
+ "maximumWarning": "500kB",
+ "maximumError": "1MB"
+ },
+ {
+ "type": "anyComponentStyle",
+ "maximumWarning": "4kB",
+ "maximumError": "8kB"
+ }
+ ],
+ "outputHashing": "all"
+ },
+ "development": {
+ "optimization": false,
+ "extractLicenses": false,
+ "sourceMap": true
+ }
},
+ "defaultConfiguration": "production"
+ },
+ "serve": {
+ "builder": "@angular/build:dev-server",
"configurations": {
"production": {
- "devServerTarget": "angularblogapp:serve:production"
+ "buildTarget": "code-samples-mfe:build:production"
},
- "local": {
- "devServerTarget": "angularblogapp:serve:local"
+ "development": {
+ "buildTarget": "code-samples-mfe:build:development"
}
+ },
+ "defaultConfiguration": "development"
+ },
+ "extract-i18n": {
+ "builder": "@angular/build:extract-i18n"
+ },
+ "test": {
+ "builder": "@angular/build:karma",
+ "options": {
+ "tsConfig": "projects/code-samples-mfe/tsconfig.spec.json",
+ "inlineStyleLanguage": "scss",
+ "assets": [
+ {
+ "glob": "**/*",
+ "input": "projects/code-samples-mfe/public"
+ }
+ ],
+ "styles": [
+ "projects/code-samples-mfe/src/styles.scss"
+ ]
+ }
+ },
+ "lint": {
+ "builder": "@angular-eslint/builder:lint",
+ "options": {
+ "lintFilePatterns": [
+ "projects/code-samples-mfe/**/*.ts",
+ "projects/code-samples-mfe/**/*.html"
+ ],
+ "eslintConfig": "projects/code-samples-mfe/eslint.config.js"
}
}
}
@@ -147,5 +352,31 @@
},
"cli": {
"analytics": "f7fe95f7-84ab-47b5-8c91-01a9d9068465"
+ },
+ "schematics": {
+ "@schematics/angular:component": {
+ "type": "component"
+ },
+ "@schematics/angular:directive": {
+ "type": "directive"
+ },
+ "@schematics/angular:service": {
+ "type": "service"
+ },
+ "@schematics/angular:guard": {
+ "typeSeparator": "."
+ },
+ "@schematics/angular:interceptor": {
+ "typeSeparator": "."
+ },
+ "@schematics/angular:module": {
+ "typeSeparator": "."
+ },
+ "@schematics/angular:pipe": {
+ "typeSeparator": "."
+ },
+ "@schematics/angular:resolver": {
+ "typeSeparator": "."
+ }
}
}
diff --git a/docs/refactor-plan/config-phase.md b/docs/refactor-plan/config-phase.md
new file mode 100644
index 0000000..ac530b5
--- /dev/null
+++ b/docs/refactor-plan/config-phase.md
@@ -0,0 +1,637 @@
+# SAFE Workspace Configuration Refactor Plan
+
+**Phase**: Configuration Only (No Feature Moves)
+**Status**: Proposal - Ready for Review
+**Date**: 2025-01-18
+
+## Overview
+
+This is a concrete, minimal, and SAFE workspace configuration refactor plan focusing exclusively on workspace configuration improvements without touching any source code or moving features. The plan addresses TypeScript configurations, Angular workspace setup, ESLint boundaries, and tooling scripts to prepare for future feature migrations.
+
+## Rationale
+
+### A) TypeScript Configuration Issues
+• **Inconsistent Structure**: Current configs mix solution-style with project-specific patterns
+• **Missing Strict Flags**: `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess` missing for Angular 20
+• **Limited Path Mappings**: Only basic `"shared"` mapping; need `@shared/*` sub-library aliases
+• **Base Config Duplication**: Each project extends root config causing duplication
+
+### B) Angular Workspace & Rendering Issues
+• **SSR/SSG Hybrid Missing**: Web project needs proper hybrid rendering configuration
+• **CSR Confirmation Needed**: Admin project should explicitly be CSR-only
+• **Native Federation Gap**: Code-samples-mfe needs micro-frontend scaffold
+• **Legacy Coexistence**: Current `/src` app needs to coexist during migration
+
+### C) ESLint & Boundaries Issues
+• **Wrong Path Patterns**: Current config targets `apps/libs` but actual structure uses `projects/`
+• **Missing Angular 20 Rules**: No standalone/signals-first rules enforced
+• **Incomplete Boundaries**: Missing shared library sub-modules in boundary definitions
+
+### D) Tooling & Scripts Issues
+• **No Project Isolation**: Missing individual project commands for development
+• **Build Orchestration**: No coordinated build process for multi-project workspace
+• **Testing Gaps**: No project-specific test commands
+
+## Proposed Changes
+
+### 1. TypeScript Configuration Modernization
+
+**File: `tsconfig.json`**
+- Convert to solution-style configuration
+- Add project references for all workspace projects
+- Remove duplicated compiler options
+
+**File: `tsconfig.base.json` (NEW)**
+- Central base configuration for all projects
+- Add missing Angular 20 strict flags
+- Enhanced path mappings for `@shared/*` aliases
+
+### 2. Angular Workspace Configuration
+
+**File: `angular.json`**
+- **Web Project**: Add hybrid SSR/SSG configuration with prerender routes
+- **Code-samples-mfe**: Add custom webpack config for Native Federation support
+- **Asset Management**: Proper federation manifest handling
+
+### 3. ESLint & Architectural Boundaries
+
+**File: `eslintrc.cjs`**
+- Fix boundary patterns to match actual `projects/` structure
+- Add Angular 20 specific rules (`prefer-standalone`, `prefer-signals`)
+- Enhanced shared library boundaries for sub-modules
+- Template rules for control flow and self-closing tags
+
+### 4. Package.json Scripts Enhancement
+
+**File: `package.json`**
+- **PNPM Setup**: Add preinstall hook for package manager enforcement
+- **Project-Specific Commands**: Individual serve/build/test commands per project
+- **Build Orchestration**: Coordinated build process with dependencies
+- **Development Workflow**: Enhanced local development commands
+
+### 5. Native Federation Scaffold
+
+**File: `projects/web/webpack.config.js` (NEW)**
+- Shell application configuration
+- Remote micro-frontend consumption setup
+- Shared dependency configuration
+
+**File: `projects/code-samples-mfe/webpack.config.js` (NEW)**
+- Micro-frontend exposition configuration
+- Module federation setup for code samples
+
+## Detailed Configuration Changes
+
+### TypeScript Base Configuration
+
+```diff
+--- /dev/null
++++ b/tsconfig.base.json
+@@ -0,0 +1,42 @@
++{
++ "compileOnSave": false,
++ "compilerOptions": {
++ "outDir": "./dist/out-tsc",
++ "strict": true,
++ "exactOptionalPropertyTypes": true,
++ "noUncheckedIndexedAccess": true,
++ "noImplicitOverride": true,
++ "noPropertyAccessFromIndexSignature": true,
++ "noImplicitReturns": true,
++ "noFallthroughCasesInSwitch": true,
++ "skipLibCheck": true,
++ "esModuleInterop": true,
++ "allowSyntheticDefaultImports": true,
++ "sourceMap": true,
++ "declaration": false,
++ "experimentalDecorators": true,
++ "moduleResolution": "bundler",
++ "paths": {
++ "shared": ["./dist/shared"],
++ "@shared/ui": ["./projects/shared/src/ui"],
++ "@shared/pattern": ["./projects/shared/src/pattern"],
++ "@shared/data-access": ["./projects/shared/src/data-access"],
++ "@shared/models": ["./projects/shared/src/models"],
++ "@shared/utils": ["./projects/shared/src/utils"]
++ },
++ "importHelpers": true,
++ "target": "ES2022",
++ "module": "ES2022",
++ "useDefineForClassFields": false,
++ "lib": [
++ "ES2022",
++ "dom"
++ ]
++ },
++ "angularCompilerOptions": {
++ "enableI18nLegacyMessageIdFormat": false,
++ "strictInjectionParameters": true,
++ "strictInputAccessModifiers": true,
++ "strictTemplates": true
++ }
++}
+```
+
+### Solution-Style Root Configuration
+
+```diff
+--- a/tsconfig.json
++++ b/tsconfig.json
+@@ -1,8 +1,5 @@
+-/* To learn more about this file see: https://angular.io/config/tsconfig. */
+ {
+ "compileOnSave": false,
+- "compilerOptions": {
+- "outDir": "./dist/out-tsc",
++ "extends": "./tsconfig.base.json",
++ "files": [],
++ "references": [
++ { "path": "./projects/admin/tsconfig.app.json" },
++ { "path": "./projects/admin/tsconfig.spec.json" },
++ { "path": "./projects/web/tsconfig.app.json" },
++ { "path": "./projects/web/tsconfig.spec.json" },
++ { "path": "./projects/shared/tsconfig.lib.json" },
++ { "path": "./projects/shared/tsconfig.spec.json" },
++ { "path": "./projects/code-samples-mfe/tsconfig.app.json" },
++ { "path": "./projects/code-samples-mfe/tsconfig.spec.json" },
++ { "path": "./tsconfig.app.json" },
++ { "path": "./tsconfig.spec.json" }
++ ]
++}
+```
+
+### Angular Workspace Hybrid Rendering
+
+```diff
+--- a/angular.json
++++ b/angular.json
+@@ -256,8 +256,15 @@
+ "styles": [
+ "projects/web/src/styles.scss"
+ ],
++ "scripts": [],
+ "server": "projects/web/src/main.server.ts",
+- "outputMode": "server",
++ "prerender": {
++ "routesFile": "projects/web/routes.txt"
++ },
+ "ssr": {
+ "entry": "projects/web/src/server.ts"
+ }
+@@ -371,7 +378,12 @@
+ "browser": "projects/code-samples-mfe/src/main.ts",
+ "tsConfig": "projects/code-samples-mfe/tsconfig.app.json",
+ "inlineStyleLanguage": "scss",
++ "customWebpackConfig": {
++ "path": "projects/code-samples-mfe/webpack.config.js"
++ },
+ "assets": [
++ {
++ "glob": "mf.manifest.json",
++ "input": "projects/code-samples-mfe/src",
++ "output": "./"
++ },
+ {
+ "glob": "**/*",
+ "input": "projects/code-samples-mfe/public"
+```
+
+### ESLint Boundary Corrections
+
+```diff
+--- a/eslintrc.cjs
++++ b/eslintrc.cjs
+@@ -15,6 +15,7 @@ module.exports = {
+ },
+ extends: [
+ 'plugin:@angular-eslint/recommended',
++ 'plugin:@angular-eslint/template/process-inline-templates',
+ ],
+ plugins: ['boundaries'],
+ settings: {
+ 'boundaries/elements': [
+ // ---- Shared libs ----
+- { type: 'shared-ui', pattern: 'libs/shared/ui/**' },
+- { type: 'shared-pattern', pattern: 'libs/shared/pattern/**' },
+- { type: 'shared-data', pattern: 'libs/shared/data-access/**' },
++ { type: 'shared-ui', pattern: 'projects/shared/src/ui/**' },
++ { type: 'shared-pattern', pattern: 'projects/shared/src/pattern/**' },
++ { type: 'shared-data', pattern: 'projects/shared/src/data-access/**' },
++ { type: 'shared-models', pattern: 'projects/shared/src/models/**' },
++ { type: 'shared-utils', pattern: 'projects/shared/src/utils/**' },
+
+- // ---- Apps: blog-ssg ----
+- { type: 'blog-core', pattern: 'apps/blog-ssg/src/app/core/**' },
+- { type: 'blog-layout', pattern: 'apps/blog-ssg/src/app/layout/**' },
+- { type: 'blog-ui', pattern: 'apps/blog-ssg/src/app/ui/**' },
+- { type: 'blog-pattern', pattern: 'apps/blog-ssg/src/app/pattern/**' },
+- { type: 'blog-data', pattern: 'apps/blog-ssg/src/app/data-access/**' },
+- { type: 'blog-feature', pattern: 'apps/blog-ssg/src/app/feature/**' },
++ // ---- Apps: web ----
++ { type: 'web-core', pattern: 'projects/web/src/app/core/**' },
++ { type: 'web-layout', pattern: 'projects/web/src/app/layout/**' },
++ { type: 'web-ui', pattern: 'projects/web/src/app/ui/**' },
++ { type: 'web-pattern', pattern: 'projects/web/src/app/pattern/**' },
++ { type: 'web-feature', pattern: 'projects/web/src/app/features/**' },
+
+- // ---- Apps: admin-spa ----
+- { type: 'admin-core', pattern: 'apps/admin-spa/src/app/core/**' },
+- { type: 'admin-layout', pattern: 'apps/admin-spa/src/app/layout/**' },
+- { type: 'admin-ui', pattern: 'apps/admin-spa/src/app/ui/**' },
+- { type: 'admin-pattern', pattern: 'apps/admin-spa/src/app/pattern/**' },
+- { type: 'admin-data', pattern: 'apps/admin-spa/src/app/data-access/**' },
+- { type: 'admin-feature', pattern: 'apps/admin-spa/src/app/feature/**' },
++ // ---- Apps: admin ----
++ { type: 'admin-core', pattern: 'projects/admin/src/app/core/**' },
++ { type: 'admin-layout', pattern: 'projects/admin/src/app/layout/**' },
++ { type: 'admin-ui', pattern: 'projects/admin/src/app/ui/**' },
++ { type: 'admin-pattern', pattern: 'projects/admin/src/app/pattern/**' },
++ { type: 'admin-feature', pattern: 'projects/admin/src/app/features/**' },
+
+- // ---- Apps: code-samples-mfe ----
+- { type: 'samples-core', pattern: 'apps/code-samples-mfe/src/app/core/**' },
+- { type: 'samples-layout', pattern: 'apps/code-samples-mfe/src/app/layout/**' },
+- { type: 'samples-ui', pattern: 'apps/code-samples-mfe/src/app/ui/**' },
+- { type: 'samples-pattern', pattern: 'apps/code-samples-mfe/src/app/pattern/**' },
+- { type: 'samples-data', pattern: 'apps/code-samples-mfe/src/app/data-access/**' },
+- { type: 'samples-feature', pattern: 'apps/code-samples-mfe/src/app/feature/**' }
++ // ---- Apps: code-samples-mfe ----
++ { type: 'mfe-core', pattern: 'projects/code-samples-mfe/src/app/core/**' },
++ { type: 'mfe-ui', pattern: 'projects/code-samples-mfe/src/app/ui/**' },
++ { type: 'mfe-feature', pattern: 'projects/code-samples-mfe/src/app/features/**' }
+ ],
+ },
+ rules: {
+ // Prevent unknown files from bypassing the rules
+- 'boundaries/no-unknown-files': 'error',
++ 'boundaries/no-unknown-files': 'warn',
+
+ // Forbid private imports across elements
+ 'boundaries/no-private': 'error',
++
++ // Angular 20 specific rules
++ '@angular-eslint/prefer-standalone': 'error',
++ '@angular-eslint/prefer-signals': 'warn',
+
+ // Allowed import graph
+ 'boundaries/allowed-types': ['error', [
+ // Shared layers
+ { from: ['shared-ui'], allow: [] },
+- { from: ['shared-pattern'], allow: ['shared-ui'] },
++ { from: ['shared-pattern'], allow: ['shared-ui', 'shared-models', 'shared-utils'] },
+ { from: ['shared-data'], allow: [] },
++ { from: ['shared-models'], allow: [] },
++ { from: ['shared-utils'], allow: [] },
+
+- // blog-ssg
+- { from: ['blog-core'], allow: ['shared-data'] },
+- { from: ['blog-layout'], allow: ['blog-core', 'shared-ui', 'shared-pattern'] },
+- { from: ['blog-ui'], allow: ['shared-ui'] },
+- { from: ['blog-pattern'], allow: ['shared-ui', 'shared-pattern', 'shared-data'] },
+- { from: ['blog-data'], allow: ['shared-data'] },
+- { from: ['blog-feature'], allow: ['blog-core', 'blog-layout', 'blog-ui', 'blog-pattern', 'blog-data', 'shared-ui', 'shared-pattern', 'shared-data'] },
++ // web app
++ { from: ['web-core'], allow: ['shared-data', 'shared-models', 'shared-utils'] },
++ { from: ['web-layout'], allow: ['web-core', 'shared-ui', 'shared-pattern', 'shared-models'] },
++ { from: ['web-ui'], allow: ['shared-ui', 'shared-models'] },
++ { from: ['web-pattern'], allow: ['shared-ui', 'shared-pattern', 'shared-data', 'shared-models', 'shared-utils'] },
++ { from: ['web-feature'], allow: ['web-core', 'web-layout', 'web-ui', 'web-pattern', 'shared-ui', 'shared-pattern', 'shared-data', 'shared-models', 'shared-utils'] },
+
+ // admin-spa
+- { from: ['admin-core'], allow: ['shared-data'] },
+- { from: ['admin-layout'], allow: ['admin-core', 'shared-ui', 'shared-pattern'] },
+- { from: ['admin-ui'], allow: ['shared-ui'] },
+- { from: ['admin-pattern'], allow: ['shared-ui', 'shared-pattern', 'shared-data'] },
+- { from: ['admin-data'], allow: ['shared-data'] },
+- { from: ['admin-feature'], allow: ['admin-core', 'admin-layout', 'admin-ui', 'admin-pattern', 'admin-data', 'shared-ui', 'shared-pattern', 'shared-data'] },
++ { from: ['admin-core'], allow: ['shared-data', 'shared-models', 'shared-utils'] },
++ { from: ['admin-layout'], allow: ['admin-core', 'shared-ui', 'shared-pattern', 'shared-models'] },
++ { from: ['admin-ui'], allow: ['shared-ui', 'shared-models'] },
++ { from: ['admin-pattern'], allow: ['shared-ui', 'shared-pattern', 'shared-data', 'shared-models', 'shared-utils'] },
++ { from: ['admin-feature'], allow: ['admin-core', 'admin-layout', 'admin-ui', 'admin-pattern', 'shared-ui', 'shared-pattern', 'shared-data', 'shared-models', 'shared-utils'] },
+
+- // samples-mfe
+- { from: ['samples-core'], allow: ['shared-data'] },
+- { from: ['samples-layout'], allow: ['samples-core', 'shared-ui', 'shared-pattern'] },
+- { from: ['samples-ui'], allow: ['shared-ui'] },
+- { from: ['samples-pattern'], allow: ['shared-ui', 'shared-pattern', 'shared-data'] },
+- { from: ['samples-data'], allow: ['shared-data'] },
+- { from: ['samples-feature'], allow: ['samples-core', 'samples-layout', 'samples-ui', 'samples-pattern', 'samples-data', 'shared-ui', 'shared-pattern', 'shared-data'] }
++ // code-samples-mfe
++ { from: ['mfe-core'], allow: ['shared-data', 'shared-models', 'shared-utils'] },
++ { from: ['mfe-ui'], allow: ['shared-ui', 'shared-models'] },
++ { from: ['mfe-feature'], allow: ['mfe-core', 'mfe-ui', 'shared-ui', 'shared-pattern', 'shared-data', 'shared-models', 'shared-utils'] }
+ ]],
+
+ // Enforce public APIs (optional): only import from directories' public entry points
+- // 'boundaries/entry-point': ['error', [{ target: 'always', from: ['shared-ui', 'shared-pattern', 'shared-data'] }]]
++ 'boundaries/entry-point': ['warn', [{ target: 'always', from: ['shared-ui', 'shared-pattern', 'shared-data', 'shared-models', 'shared-utils'] }]]
+ },
+ },
++ {
++ files: ['*.html'],
++ extends: ['plugin:@angular-eslint/template/recommended'],
++ rules: {
++ '@angular-eslint/template/prefer-control-flow': 'error',
++ '@angular-eslint/template/prefer-self-closing-tags': 'error'
++ }
++ }
+ ]
+ };
+```
+
+### Enhanced Package.json Scripts
+
+```diff
+--- a/package.json
++++ b/package.json
+@@ -2,6 +2,7 @@
+ "name": "angular-blog-app",
+ "version": "0.0.0",
+ "scripts": {
++ "preinstall": "npx only-allow pnpm",
+ "ng": "ng",
+ "start": "ng serve",
++ "start:web": "ng serve web",
++ "start:admin": "ng serve admin",
++ "start:mfe": "ng serve code-samples-mfe",
+ "build": "ng build",
++ "build:web": "ng build web",
++ "build:admin": "ng build admin",
++ "build:mfe": "ng build code-samples-mfe",
++ "build:shared": "ng build shared",
++ "build:all": "pnpm build:shared && pnpm build:web && pnpm build:admin && pnpm build:mfe",
+ "watch": "ng build --watch --configuration development",
++ "watch:web": "ng build web --watch --configuration development",
++ "watch:admin": "ng build admin --watch --configuration development",
++ "watch:mfe": "ng build code-samples-mfe --watch --configuration development",
+ "test": "ng test",
++ "test:web": "ng test web",
++ "test:admin": "ng test admin",
++ "test:mfe": "ng test code-samples-mfe",
++ "test:shared": "ng test shared",
++ "lint": "ng lint",
++ "lint:fix": "ng lint --fix",
+ "serve:ssr:AngularBlogApp": "node dist/angular-blog-app/server/server.mjs",
++ "serve:ssr:web": "node dist/web/server/server.mjs",
+ "build:stats": "ng build --stats-json",
++ "build:web:stats": "ng build web --stats-json",
+ "analyze": "webpack-bundle-analyzer dist/angular-blog-app/stats.json",
++ "analyze:web": "webpack-bundle-analyzer dist/web/stats.json",
+ "start:local": "ng serve --configuration=local",
++ "start:web:local": "ng serve web --configuration=local",
+ "start:local:docker": "open -a Docker",
+ "start:local:backend": "npx supabase start",
+ "schema:pull": "find supabase/migrations -name '*_remote_schema.sql' -delete && supabase db pull --db-url $PG_EXPORT_URL",
+ "db:createSeed": "scripts/create-seed.sh",
+ "db:seed": "npx @snaplet/seed init",
+ "users:passwords": "scripts/set-passwords.sh",
+ "e2e": "ng e2e",
+ "e2e:local": "ng e2e -c local",
+- "serve:ssr:web": "node dist/web/server/server.mjs"
++ "e2e:web": "ng e2e web",
++ "e2e:admin": "ng e2e admin"
+ },
+ "private": true,
+ "dependencies": {
+@@ -51,6 +77,7 @@
+ "devDependencies": {
+ "@angular/build": "^20.1.6",
+ "@angular/cli": "^20.1.6",
+ "@angular/compiler-cli": "^20.1.7",
++ "@angular-eslint/eslint-plugin": "^18.0.0",
++ "eslint-plugin-boundaries": "^4.0.0",
+ "@playwright/test": "^1.53.0",
+ "@snaplet/copycat": "^6.0.0",
+```
+
+### Native Federation Configuration
+
+**Shell Application (Web) - `projects/web/webpack.config.js`**
+
+```javascript
+const ModuleFederationPlugin = require('@module-federation/enhanced/webpack');
+
+module.exports = {
+ plugins: [
+ new ModuleFederationPlugin({
+ name: 'shell',
+ filename: 'remoteEntry.js',
+ remotes: {
+ 'code-samples-mfe': 'http://localhost:4201/remoteEntry.js'
+ },
+ shared: {
+ '@angular/core': { singleton: true, strictVersion: true },
+ '@angular/common': { singleton: true, strictVersion: true },
+ '@angular/router': { singleton: true, strictVersion: true },
+ 'rxjs': { singleton: true, strictVersion: true }
+ }
+ })
+ ]
+};
+```
+
+**Micro-frontend (Code Samples) - `projects/code-samples-mfe/webpack.config.js`**
+
+```javascript
+const ModuleFederationPlugin = require('@module-federation/enhanced/webpack');
+
+module.exports = {
+ plugins: [
+ new ModuleFederationPlugin({
+ name: 'codeSamplesMfe',
+ filename: 'remoteEntry.js',
+ exposes: {
+ './CodeSamplesModule': './src/app/features/code-samples/code-samples.module.ts'
+ },
+ shared: {
+ '@angular/core': { singleton: true, strictVersion: true },
+ '@angular/common': { singleton: true, strictVersion: true },
+ '@angular/router': { singleton: true, strictVersion: true },
+ 'rxjs': { singleton: true, strictVersion: true }
+ }
+ })
+ ]
+};
+```
+
+## Implementation Order
+
+### Phase 1: TypeScript Foundation
+1. Create `tsconfig.base.json`
+2. Update `tsconfig.json` to solution-style
+3. Verify compilation with `ng build`
+
+### Phase 2: Angular Workspace
+1. Update `angular.json` with SSR/SSG and webpack configs
+2. Test project builds individually
+3. Verify dev servers start correctly
+
+### Phase 3: ESLint Boundaries
+1. Update `eslintrc.cjs` with correct patterns
+2. Run lint to identify boundary violations
+3. Address any immediate issues
+
+### Phase 4: Package Scripts
+1. Add project-specific scripts to `package.json`
+2. Install missing dependencies
+3. Test new script commands
+
+### Phase 5: Native Federation
+1. Add webpack config files
+2. Install Module Federation dependencies
+3. Test federation setup with minimal examples
+
+## Verification Checklist
+
+### TypeScript Compilation
+```bash
+pnpm build:shared # Should succeed
+pnpm build:web # Should succeed with SSR
+pnpm build:admin # Should succeed CSR-only
+pnpm build:mfe # Should succeed with webpack config
+```
+
+### Development Servers
+```bash
+pnpm start:web # Should serve on :4200 with SSR
+pnpm start:admin # Should serve on :4201 CSR-only
+pnpm start:mfe # Should serve on :4202 with federation
+```
+
+### Linting
+```bash
+pnpm lint # Should pass with new boundary rules
+pnpm lint:fix # Should auto-fix minor issues
+```
+
+### Path Mappings
+```bash
+# In any project file: import { Something } from '@shared/ui'
+# Should resolve without TypeScript errors
+```
+
+### Angular CLI Recognition
+```bash
+ng serve web # Should work
+ng build admin # Should work
+ng test shared # Should work
+ng lint # Should enforce boundaries
+```
+
+### Federation Testing
+```bash
+# Start both applications
+pnpm start:web # Shell app on :4200
+pnpm start:mfe # Remote MFE on :4201
+
+# Verify federation manifest files are generated
+ls projects/code-samples-mfe/dist/mf.manifest.json
+```
+
+## Risks and Mitigation
+
+### High Risk Items
+1. **TypeScript Path Changes** - Might break existing imports temporarily
+ - **Mitigation**: Apply changes incrementally, test compilation after each step
+
+2. **Native Federation Dependencies** - New webpack config might conflict
+ - **Mitigation**: Use `customWebpackConfig` option, preserve existing configs
+
+3. **ESLint Boundary Violations** - Existing code might violate new rules
+ - **Mitigation**: Set violations to 'warn' initially, fix gradually
+
+### Medium Risk Items
+1. **Package Manager Switch** - PNPM enforcement might cause issues
+ - **Mitigation**: Optional preinstall hook, can be disabled
+
+2. **Build Process Changes** - New scripts might not work as expected
+ - **Mitigation**: Keep original scripts as fallback
+
+### Low Risk Items
+1. **Development Server Ports** - Port conflicts with existing services
+ - **Mitigation**: Use standard Angular CLI ports, configurable
+
+## Rollback Strategy
+
+### Emergency Rollback
+```bash
+git stash # Save current changes
+git reset --hard HEAD~1 # Return to previous commit
+```
+
+### Incremental Rollback
+
+**TypeScript Changes**
+```bash
+# Remove tsconfig.base.json
+rm tsconfig.base.json
+# Restore original tsconfig.json from git
+git checkout HEAD~1 -- tsconfig.json
+```
+
+**Angular.json Changes**
+```bash
+# Restore specific sections
+git checkout HEAD~1 -- angular.json
+```
+
+**ESLint Changes**
+```bash
+# Restore original patterns
+git checkout HEAD~1 -- eslintrc.cjs
+```
+
+**Package.json Changes**
+```bash
+# Restore original scripts section
+git checkout HEAD~1 -- package.json
+npm install # or pnpm install
+```
+
+### Dependency Issues
+```bash
+# Remove new dependencies
+pnpm remove @angular-eslint/eslint-plugin eslint-plugin-boundaries
+# Reinstall original dependencies
+pnpm install
+```
+
+## Post-Implementation Tasks
+
+### Immediate (Day 1)
+- [ ] Run full verification checklist
+- [ ] Update team documentation
+- [ ] Create quick reference for new commands
+
+### Short Term (Week 1)
+- [ ] Monitor build performance impact
+- [ ] Address any ESLint boundary violations
+- [ ] Train team on new development workflow
+
+### Medium Term (Month 1)
+- [ ] Optimize build configurations
+- [ ] Add more comprehensive Native Federation examples
+- [ ] Plan next phase: feature migrations
+
+## Success Criteria
+
+✅ **All existing functionality preserved**
+✅ **No source code changes required**
+✅ **Improved development workflow with project isolation**
+✅ **Foundation ready for feature migrations**
+✅ **Clear path mapping strategy established**
+✅ **Architectural boundaries enforced via tooling**
+✅ **Native Federation infrastructure in place**
+
+## Next Phase Preparation
+
+This configuration refactor prepares for the next phase:
+- **Feature Migration Phase**: Moving features from `/src` to appropriate projects
+- **Shared Library Population**: Creating actual shared components and services
+- **Native Federation Implementation**: Actual micro-frontend integration
+- **SSR/SSG Optimization**: Performance tuning for hybrid rendering
+
+---
+
+**Ready for Review**: This plan can be implemented incrementally with clear rollback options at each step.
\ No newline at end of file
diff --git a/e2e/admin/admin.spec.ts b/e2e/admin/admin.spec.ts
new file mode 100644
index 0000000..86c775c
--- /dev/null
+++ b/e2e/admin/admin.spec.ts
@@ -0,0 +1,13 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Admin Application', () => {
+ test('admin app loads', async ({ page }) => {
+ await page.goto('/');
+ await expect(page).toHaveTitle(/Admin/);
+ });
+
+ test('displays admin dashboard', async ({ page }) => {
+ await page.goto('/');
+ await expect(page.getByText('Admin')).toBeVisible();
+ });
+});
\ No newline at end of file
diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts
deleted file mode 100644
index ef49468..0000000
--- a/e2e/example.spec.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { test, expect } from '@playwright/test';
-import { acceptCookies } from './helpers/cookie-consent.helper';
-
-test('has title', async ({ page }) => {
- await page.goto('/');
-
- // Handle cookie consent using helper
- await acceptCookies(page);
-
- // Expect a title "to contain" a substring.
- await expect(page).toHaveTitle(/AngularBlogApp/);
-});
diff --git a/e2e/tags.spec.ts b/e2e/web/tags.spec.ts
similarity index 62%
rename from e2e/tags.spec.ts
rename to e2e/web/tags.spec.ts
index 3593065..91ebde6 100644
--- a/e2e/tags.spec.ts
+++ b/e2e/web/tags.spec.ts
@@ -1,6 +1,6 @@
import { test, expect } from '@playwright/test';
-import { acceptCookies } from './helpers/cookie-consent.helper';
-import { waitForAngularToLoad, waitForApiCall } from './helpers/debug.helper';
+import { acceptCookies } from '../helpers/cookie-consent.helper';
+import { waitForAngularToLoad, waitForApiCall } from '../helpers/debug.helper';
test.describe('Tags Display and API', () => {
test('should display tags and verify API call', async ({ page }) => {
@@ -11,13 +11,9 @@ test.describe('Tags Display and API', () => {
status?: number;
}> = [];
- page.on('request', (request) => {
+ page.on('request', request => {
const url = request.url();
- if (
- url.includes('/rest/v1/tags') ||
- url.includes('supabase') ||
- url.includes('tag')
- ) {
+ if (url.includes('/rest/v1/tags') || url.includes('supabase') || url.includes('tag')) {
apiRequests.push({
url,
method: request.method(),
@@ -26,10 +22,10 @@ test.describe('Tags Display and API', () => {
}
});
- page.on('response', (response) => {
+ page.on('response', response => {
const url = response.url();
if (url.includes('/rest/v1/tags')) {
- const existingRequest = apiRequests.find((req) => req.url === url);
+ const existingRequest = apiRequests.find(req => req.url === url);
if (existingRequest) {
existingRequest.status = response.status();
}
@@ -37,20 +33,18 @@ test.describe('Tags Display and API', () => {
});
await page.goto('/', { waitUntil: 'networkidle' });
- await acceptCookies(page);
- await waitForAngularToLoad(page, 500);
- await page.waitForSelector('[data-testid="tags-container"]', {
- timeout: 500,
- });
+ // await acceptCookies(page);
+ // await waitForAngularToLoad(page, 50);
+ // await page.getByTestId('tags-container')
try {
- await waitForApiCall(page, '/rest/v1/tags', 300);
+ await waitForApiCall(page, '/rest/v1/tags', 30);
} catch (error) {
// Continue test even if API call detection fails
}
- await page.waitForTimeout(1000);
- await page.waitForSelector('[data-testid="tag-item"]', { timeout: 500 });
+ // await page.waitForTimeout(100);
+ // await page.waitForSelector('[data-testid="tag-item"]', { timeout: 500 });
const tagsContainer = page.locator('[data-testid="tags-container"]');
await expect(tagsContainer).toBeVisible();
@@ -62,9 +56,7 @@ test.describe('Tags Display and API', () => {
];
for (const tag of expectedTags) {
- const tagItem = page.locator(
- `[data-testid="tag-item"][data-tag-name="${tag.name}"]`,
- );
+ const tagItem = page.locator(`[data-testid="tag-item"][data-tag-name="${tag.name}"]`);
await expect(tagItem).toBeVisible();
const tagName = tagItem.locator('[data-testid="tag-name"]');
diff --git a/src/environments/environment.development.ts b/environments/environment.development.ts
similarity index 100%
rename from src/environments/environment.development.ts
rename to environments/environment.development.ts
diff --git a/src/environments/environment.local.ts b/environments/environment.local.ts
similarity index 100%
rename from src/environments/environment.local.ts
rename to environments/environment.local.ts
diff --git a/src/environments/environment.ts b/environments/environment.ts
similarity index 100%
rename from src/environments/environment.ts
rename to environments/environment.ts
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..7688934
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,302 @@
+// @ts-check
+const eslint = require('@eslint/js');
+const tseslint = require('typescript-eslint');
+const angular = require('angular-eslint');
+const boundaries = require('eslint-plugin-boundaries');
+const importPlugin = require('eslint-plugin-import');
+
+module.exports = tseslint.config(
+ {
+ ignores: [
+ 'dist/**',
+ 'node_modules/**',
+ 'coverage/**',
+ '.angular/**',
+ 'src/**',
+ '**/*.spec.ts',
+ '**/e2e/**',
+ '**/*.generated.ts',
+ '**/*.d.ts',
+ '!**/*.spec.d.ts',
+ '*.config.js',
+ '*.config.ts',
+ ],
+ },
+ {
+ files: ['**/*.ts'],
+ extends: [
+ eslint.configs.recommended,
+ ...tseslint.configs.recommended,
+ ...tseslint.configs.stylistic,
+ ...angular.configs.tsRecommended,
+ ],
+ processor: angular.processInlineTemplates,
+ plugins: {
+ boundaries,
+ import: importPlugin,
+ },
+ settings: {
+ 'import/resolver': {
+ typescript: {
+ alwaysTryTypes: true,
+ project: ['tsconfig.json', 'projects/*/tsconfig*.json'],
+ },
+ },
+ 'boundaries/dependency-nodes': ['import', 'dynamic-import'],
+ 'boundaries/elements': [
+ { type: 'shared-lib', pattern: 'projects/shared/src/lib/**' },
+ { type: 'shared-ui', pattern: 'projects/shared/src/ui/**' },
+ { type: 'shared-pattern', pattern: 'projects/shared/src/pattern/**' },
+ { type: 'shared-services', pattern: 'projects/shared/src/services/**' },
+ { type: 'shared-data-access', pattern: 'projects/shared/src/data-access/**' },
+ { type: 'shared-models', pattern: 'shared/models' },
+ { type: 'shared-utils', pattern: 'shared/utils' },
+ { type: 'shared-public-api', mode: 'file', pattern: 'projects/shared/src/public-api.ts' },
+ { type: 'shared-external', mode: 'file', pattern: 'shared' },
+ { type: 'web-main', mode: 'file', pattern: 'projects/web/src/main.ts' },
+ { type: 'web-main-server', mode: 'file', pattern: 'projects/web/src/main.server.ts' },
+ { type: 'web-server', mode: 'file', pattern: 'projects/web/src/server.ts' },
+ { type: 'web-app', mode: 'file', pattern: 'projects/web/src/app/app*.ts' },
+ { type: 'web-core', pattern: 'projects/web/src/app/core/**' },
+ { type: 'web-layout', pattern: 'projects/web/src/app/layout/**' },
+ { type: 'web-ui', pattern: 'projects/web/src/app/ui/**' },
+ { type: 'web-pattern', pattern: 'projects/web/src/app/pattern/**' },
+ {
+ type: 'web-feature-routes',
+ mode: 'file',
+ pattern: 'projects/web/src/app/features/*/*.routes.ts',
+ capture: ['feature'],
+ },
+ { type: 'web-feature', pattern: 'projects/web/src/app/features/**', capture: ['feature'] },
+ { type: 'admin-main', mode: 'file', pattern: 'projects/admin/src/main.ts' },
+ { type: 'admin-app', mode: 'file', pattern: 'projects/admin/src/app/app*.ts' },
+ { type: 'admin-core', pattern: 'projects/admin/src/app/core/**' },
+ { type: 'admin-layout', pattern: 'projects/admin/src/app/layout/**' },
+ { type: 'admin-ui', pattern: 'projects/admin/src/app/ui/**' },
+ { type: 'admin-pattern', pattern: 'projects/admin/src/app/pattern/**' },
+ {
+ type: 'admin-feature-routes',
+ mode: 'file',
+ pattern: 'projects/admin/src/app/features/*/*.routes.ts',
+ capture: ['feature'],
+ },
+ {
+ type: 'admin-feature',
+ pattern: 'projects/admin/src/app/features/**',
+ capture: ['feature'],
+ },
+ { type: 'mfe-main', mode: 'file', pattern: 'projects/code-samples-mfe/src/main.ts' },
+ { type: 'mfe-app', mode: 'file', pattern: 'projects/code-samples-mfe/src/app/app*.ts' },
+ { type: 'mfe-core', pattern: 'projects/code-samples-mfe/src/app/core/**' },
+ { type: 'mfe-ui', pattern: 'projects/code-samples-mfe/src/app/ui/**' },
+ {
+ type: 'mfe-feature-routes',
+ mode: 'file',
+ pattern: 'projects/code-samples-mfe/src/app/features/*/*.routes.ts',
+ capture: ['feature'],
+ },
+ {
+ type: 'mfe-feature',
+ pattern: 'projects/code-samples-mfe/src/app/features/**',
+ capture: ['feature'],
+ },
+ { type: 'environment', pattern: '**/environments/**' },
+ ],
+ 'boundaries/ignore': ['**/*.spec.ts', '**/e2e/**', '**/*.config.*'],
+ },
+ rules: {
+ '@angular-eslint/prefer-standalone': 'error',
+ '@angular-eslint/prefer-signals': 'warn',
+ 'import/order': [
+ 'error',
+ {
+ groups: ['builtin', 'external', ['internal', 'parent', 'sibling', 'index']],
+ 'newlines-between': 'never',
+ },
+ ],
+ 'boundaries/no-unknown': 'error',
+ 'boundaries/no-private': 'error',
+ 'boundaries/no-unknown-files': 'error',
+ 'boundaries/element-types': [
+ 'error',
+ {
+ default: 'disallow',
+ rules: [
+ { from: 'shared-lib', allow: [] },
+ { from: 'shared-ui', allow: ['shared-models'] },
+ { from: 'shared-pattern', allow: ['shared-ui', 'shared-models', 'shared-utils'] },
+ { from: 'shared-services', allow: ['shared-models', 'shared-utils'] },
+ { from: 'shared-data-access', allow: ['shared-models'] },
+ { from: 'shared-models', allow: [] },
+ { from: 'shared-utils', allow: [] },
+ {
+ from: 'shared-public-api',
+ allow: [
+ 'shared-lib',
+ 'shared-ui',
+ 'shared-pattern',
+ 'shared-services',
+ 'shared-data-access',
+ 'shared-models',
+ 'shared-utils',
+ ],
+ },
+ { from: 'shared-external', allow: [] },
+
+ { from: 'web-main', allow: ['web-app'] },
+ { from: 'web-main-server', allow: ['web-app'] },
+ { from: 'web-server', allow: ['web-app', 'shared-public-api'] },
+ {
+ from: 'web-app',
+ allow: [
+ 'web-core',
+ 'web-layout',
+ 'web-feature-routes',
+ 'web-feature',
+ 'shared-public-api',
+ ],
+ },
+ { from: 'web-core', allow: ['shared-public-api', 'environment'] },
+ {
+ from: 'web-layout',
+ allow: ['web-core', 'shared-ui', 'shared-pattern', 'shared-public-api'],
+ },
+ { from: 'web-ui', allow: ['shared-ui', 'shared-models'] },
+ {
+ from: 'web-pattern',
+ allow: ['web-core', 'shared-ui', 'shared-pattern', 'shared-public-api'],
+ },
+ {
+ from: 'web-feature',
+ allow: ['web-core', 'web-ui', 'web-pattern', 'shared-public-api'],
+ },
+ { from: 'web-feature-routes', allow: ['web-core', 'web-pattern', 'web-feature'] },
+
+ { from: 'admin-main', allow: ['admin-app'] },
+ {
+ from: 'admin-app',
+ allow: ['admin-core', 'admin-layout', 'admin-feature-routes', 'shared-public-api'],
+ },
+ { from: 'admin-core', allow: ['shared-public-api', 'environment'] },
+ {
+ from: 'admin-layout',
+ allow: ['admin-core', 'shared-ui', 'shared-pattern', 'shared-public-api'],
+ },
+ { from: 'admin-ui', allow: ['shared-ui', 'shared-models'] },
+ {
+ from: 'admin-pattern',
+ allow: ['admin-core', 'shared-ui', 'shared-pattern', 'shared-public-api'],
+ },
+ {
+ from: 'admin-feature',
+ allow: ['admin-core', 'admin-ui', 'admin-pattern', 'shared-public-api'],
+ },
+ {
+ from: 'admin-feature-routes',
+ allow: ['admin-core', 'admin-pattern', 'admin-feature'],
+ },
+
+ { from: 'mfe-main', allow: ['mfe-app'] },
+ { from: 'mfe-app', allow: ['mfe-core', 'mfe-feature-routes', 'shared-public-api'] },
+ { from: 'mfe-core', allow: ['shared-public-api'] },
+ { from: 'mfe-ui', allow: ['shared-ui', 'shared-models'] },
+ { from: 'mfe-feature', allow: ['mfe-core', 'mfe-ui', 'shared-public-api'] },
+ { from: 'mfe-feature-routes', allow: ['mfe-core', 'mfe-feature'] },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ files: ['**/*.html'],
+ extends: [...angular.configs.templateRecommended, ...angular.configs.templateAccessibility],
+ rules: {
+ '@angular-eslint/template/prefer-control-flow': 'error',
+ '@angular-eslint/template/prefer-self-closing-tags': 'error',
+ },
+ },
+ {
+ files: ['projects/web/src/**/*.ts'],
+ rules: {
+ '@angular-eslint/directive-selector': [
+ 'error',
+ {
+ type: 'attribute',
+ prefix: ['web', 'shared'],
+ style: 'camelCase',
+ },
+ ],
+ '@angular-eslint/component-selector': [
+ 'error',
+ {
+ type: 'element',
+ prefix: ['web', 'shared'],
+ style: 'kebab-case',
+ },
+ ],
+ },
+ },
+ {
+ files: ['projects/admin/src/**/*.ts'],
+ rules: {
+ '@angular-eslint/directive-selector': [
+ 'error',
+ {
+ type: 'attribute',
+ prefix: ['admin', 'shared'],
+ style: 'camelCase',
+ },
+ ],
+ '@angular-eslint/component-selector': [
+ 'error',
+ {
+ type: 'element',
+ prefix: ['admin', 'shared'],
+ style: 'kebab-case',
+ },
+ ],
+ },
+ },
+ {
+ files: ['projects/code-samples-mfe/src/**/*.ts'],
+ rules: {
+ '@angular-eslint/directive-selector': [
+ 'error',
+ {
+ type: 'attribute',
+ prefix: ['mfe', 'shared'],
+ style: 'camelCase',
+ },
+ ],
+ '@angular-eslint/component-selector': [
+ 'error',
+ {
+ type: 'element',
+ prefix: ['mfe', 'shared'],
+ style: 'kebab-case',
+ },
+ ],
+ },
+ },
+ {
+ files: ['projects/shared/src/**/*.ts'],
+ rules: {
+ '@angular-eslint/directive-selector': [
+ 'error',
+ {
+ type: 'attribute',
+ prefix: 'shared',
+ style: 'camelCase',
+ },
+ ],
+ '@angular-eslint/component-selector': [
+ 'error',
+ {
+ type: 'element',
+ prefix: 'shared',
+ style: 'kebab-case',
+ },
+ ],
+ },
+ }
+);
diff --git a/eslintrc.cjs b/eslintrc.cjs
new file mode 100644
index 0000000..a13e10e
--- /dev/null
+++ b/eslintrc.cjs
@@ -0,0 +1,102 @@
+/* .eslintrc.cjs — boundaries for angular.fun monorepo
+ * Layers per app: core, layout, ui, pattern, data-access, feature (lazy)
+ * Shared libs: shared/ui, shared/pattern, shared/data-access
+ */
+/* eslint-env node */
+module.exports = {
+ root: true,
+ ignorePatterns: ['**/*'],
+ overrides: [
+ {
+ files: ['*.ts'],
+ parserOptions: {
+ project: ['tsconfig.json'],
+ sourceType: 'module',
+ },
+ extends: [
+ 'plugin:@angular-eslint/recommended',
+ 'plugin:@angular-eslint/template/process-inline-templates',
+ ],
+ plugins: ['boundaries'],
+ settings: {
+ 'boundaries/elements': [
+ // ---- Shared libs ----
+ { type: 'shared-ui', pattern: 'libs/shared/ui/**' },
+ { type: 'shared-pattern', pattern: 'libs/shared/pattern/**' },
+ { type: 'shared-data', pattern: 'libs/shared/data-access/**' },
+
+ // ---- Apps: blog-ssg ----
+ { type: 'blog-core', pattern: 'apps/blog-ssg/src/app/core/**' },
+ { type: 'blog-layout', pattern: 'apps/blog-ssg/src/app/layout/**' },
+ { type: 'blog-ui', pattern: 'apps/blog-ssg/src/app/ui/**' },
+ { type: 'blog-pattern', pattern: 'apps/blog-ssg/src/app/pattern/**' },
+ { type: 'blog-data', pattern: 'apps/blog-ssg/src/app/data-access/**' },
+ { type: 'blog-feature', pattern: 'apps/blog-ssg/src/app/feature/**' },
+
+ // ---- Apps: admin-spa ----
+ { type: 'admin-core', pattern: 'apps/admin-spa/src/app/core/**' },
+ { type: 'admin-layout', pattern: 'apps/admin-spa/src/app/layout/**' },
+ { type: 'admin-ui', pattern: 'apps/admin-spa/src/app/ui/**' },
+ { type: 'admin-pattern', pattern: 'apps/admin-spa/src/app/pattern/**' },
+ { type: 'admin-data', pattern: 'apps/admin-spa/src/app/data-access/**' },
+ { type: 'admin-feature', pattern: 'apps/admin-spa/src/app/feature/**' },
+
+ // ---- Apps: code-samples-mfe ----
+ { type: 'samples-core', pattern: 'apps/code-samples-mfe/src/app/core/**' },
+ { type: 'samples-layout', pattern: 'apps/code-samples-mfe/src/app/layout/**' },
+ { type: 'samples-ui', pattern: 'apps/code-samples-mfe/src/app/ui/**' },
+ { type: 'samples-pattern', pattern: 'apps/code-samples-mfe/src/app/pattern/**' },
+ { type: 'samples-data', pattern: 'apps/code-samples-mfe/src/app/data-access/**' },
+ { type: 'samples-feature', pattern: 'apps/code-samples-mfe/src/app/feature/**' }
+ ],
+ },
+ rules: {
+ // Prevent unknown files from bypassing the rules
+ 'boundaries/no-unknown-files': 'error',
+
+ // Forbid private imports across elements
+ 'boundaries/no-private': 'error',
+
+ // Allowed import graph
+ 'boundaries/allowed-types': ['error', [
+ // Shared layers
+ { from: ['shared-ui'], allow: [] },
+ { from: ['shared-pattern'], allow: ['shared-ui'] },
+ { from: ['shared-data'], allow: [] },
+
+ // blog-ssg
+ { from: ['blog-core'], allow: ['shared-data'] },
+ { from: ['blog-layout'], allow: ['blog-core', 'shared-ui', 'shared-pattern'] },
+ { from: ['blog-ui'], allow: ['shared-ui'] },
+ { from: ['blog-pattern'], allow: ['shared-ui', 'shared-pattern', 'shared-data'] },
+ { from: ['blog-data'], allow: ['shared-data'] },
+ { from: ['blog-feature'], allow: ['blog-core', 'blog-layout', 'blog-ui', 'blog-pattern', 'blog-data', 'shared-ui', 'shared-pattern', 'shared-data'] },
+
+ // admin-spa
+ { from: ['admin-core'], allow: ['shared-data'] },
+ { from: ['admin-layout'], allow: ['admin-core', 'shared-ui', 'shared-pattern'] },
+ { from: ['admin-ui'], allow: ['shared-ui'] },
+ { from: ['admin-pattern'], allow: ['shared-ui', 'shared-pattern', 'shared-data'] },
+ { from: ['admin-data'], allow: ['shared-data'] },
+ { from: ['admin-feature'], allow: ['admin-core', 'admin-layout', 'admin-ui', 'admin-pattern', 'admin-data', 'shared-ui', 'shared-pattern', 'shared-data'] },
+
+ // samples-mfe
+ { from: ['samples-core'], allow: ['shared-data'] },
+ { from: ['samples-layout'], allow: ['samples-core', 'shared-ui', 'shared-pattern'] },
+ { from: ['samples-ui'], allow: ['shared-ui'] },
+ { from: ['samples-pattern'], allow: ['shared-ui', 'shared-pattern', 'shared-data'] },
+ { from: ['samples-data'], allow: ['shared-data'] },
+ { from: ['samples-feature'], allow: ['samples-core', 'samples-layout', 'samples-ui', 'samples-pattern', 'samples-data', 'shared-ui', 'shared-pattern', 'shared-data'] }
+ ]],
+
+ // Enforce public APIs (optional): only import from directories' public entry points
+ // 'boundaries/entry-point': ['error', [{ target: 'always', from: ['shared-ui', 'shared-pattern', 'shared-data'] }]]
+ },
+ },
+ {
+ files: ['*.html'],
+ extends: ['plugin:@angular-eslint/template/recommended'],
+ rules: {}
+ }
+ ]
+};
diff --git a/firebase.json b/firebase.json
index e69de29..3285260 100644
--- a/firebase.json
+++ b/firebase.json
@@ -0,0 +1,38 @@
+{
+ "hosting": [
+ {
+ "target": "admin",
+ "public": "dist/apps/admin-spa",
+ "ignore": ["**/.*", "**/node_modules/**"],
+ "rewrites": [{ "source": "**", "destination": "/index.html" }],
+ "headers": [
+ {
+ "source": "**/*.@(js|css)",
+ "headers": [
+ { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
+ ]
+ }
+ ]
+ },
+ {
+ "target": "samples",
+ "public": "dist/apps/code-samples-mfe",
+ "ignore": ["**/.*", "**/node_modules/**"],
+ "rewrites": [{ "source": "**", "destination": "/index.html" }],
+ "headers": [
+ {
+ "source": "**/remoteEntry.@(js|mjs)",
+ "headers": [
+ { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
+ ]
+ },
+ {
+ "source": "**/*.@(js|css)",
+ "headers": [
+ { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/llms.txt b/llms.txt
deleted file mode 100644
index 47c724d..0000000
--- a/llms.txt
+++ /dev/null
@@ -1,129 +0,0 @@
-# Angular
-
-Angular — Deliver web apps with confidence 🚀
-
-## Table of Contents
-
-- [What is Angular](https://angular.dev/overview)
-- [Installation guide](https://angular.dev/installation)
-- [Style Guide](https://next.angular.dev/style-guide)
-
-## Components
-
-- [What is a component](https://angular.dev/guide/components)
-- [Component selectors](https://angular.dev/guide/components/selectors)
-- [Styling components](https://angular.dev/guide/components/styling)
-- [Accepting data with input properties](https://angular.dev/guide/components/inputs)
-- [Custom events with output](https://angular.dev/guide/components/outputs)
-- [Content projection](https://angular.dev/guide/components/content-projection)
-- [Component lifecycle](https://angular.dev/guide/components/lifecycle)
-
-## Templates guides
-
-- [Template Overview](https://angular.dev/guide/templates)
-- [Adding event listeners](https://angular.dev/guide/templates/event-listeners)
-- [Binding text, properties and attributes](https://angular.dev/guide/templates/binding)
-- [Control Flow](https://angular.dev/guide/templates/control-flow)
-- [Template variable declaration](https://angular.dev/guide/templates/variables)
-- [Deferred loading of components](https://angular.dev/guide/templates/defer)
-- [Expression syntax](https://angular.dev/guide/templates/expression-syntax)
-
-## Directives
-
-- [Directives overview](https://angular.dev/guide/directives)
-- [Attribute directives](https://angular.dev/guide/directives/attribute-directives)
-- [Structural directives](https://angular.dev/guide/directives/structural-directives)
-- [Directive composition](https://angular.dev/guide/directives/directive-composition-api)
-- [Optimizing images](https://angular.dev/guide/image-optimization)
-
-## Signals
-
-- [Signals overview](https://angular.dev/guide/signals)
-- [Dependent state with linkedSignal](https://angular.dev/guide/signals/linked-signal)
-- [Async reactivity with resources](https://angular.dev/guide/signals/resource)
-
-## Dependency injection (DI)
-
-- [Dependency Injection overview](https://angular.dev/guide/di)
-- [Understanding Dependency injection](https://angular.dev/guide/di/dependency-injection)
-- [Creating an injectable service](https://angular.dev/guide/di/creating-injectable-service)
-- [Configuring dependency providers](https://angular.dev/guide/di/dependency-injection-providers)
-- [Injection context](https://angular.dev/guide/di/dependency-injection-context)
-- [Hierarchical injectors](https://angular.dev/guide/di/hierarchical-dependency-injection)
-- [Optimizing Injection tokens](https://angular.dev/guide/di/lightweight-injection-tokens)
-
-## RxJS
-
-- [RxJS interop with Angular signals](https://angular.dev/ecosystem/rxjs-interop)
-- [Component output interop](https://angular.dev/ecosystem/rxjs-interop/output-interop)
-
-## Loading Data
-
-- [HttpClient overview](https://angular.dev/guide/http)
-- [Setting up the HttpClient](https://angular.dev/guide/http/setup)
-- [Making requests](https://angular.dev/guide/http/making-requests)
-- [Intercepting requests](https://angular.dev/guide/http/interceptors)
-- [Testing](https://angular.dev/guide/http/testing)
-
-## Forms
-- [Forms overview](https://angular.dev/guide/forms)
-- [Reactive Forms](https://angular.dev/guide/forms/reactive-forms)
-- [Strictly types forms](https://angular.dev/guide/forms/typed-forms)
-- [Template driven forms](https://angular.dev/guide/forms/template-driven-forms)
-- [Validate forms input](https://angular.dev/guide/forms/form-validation)
-- [Building dynamic forms](https://angular.dev/guide/forms/dynamic-forms)
-
-## Routing
-- [Routing overview](https://angular.dev/guide/routing)
-- [Common routing tasks](https://angular.dev/guide/routing/common-router-tasks)
-- [Routing in an SPA](https://angular.dev/guide/routing/router-tutorial)
-- [Creating custom route matches](https://angular.dev/guide/routing/routing-with-urlmatcher)
-
-## Server Side Rendering (SSR)
-
-- [SSR Overview](https://angular.dev/guide/performance)
-- [SSR with Angular](https://angular.dev/guide/ssr)
-- [Build-time prerendering (SSG)](https://angular.dev/guide/prerendering)
-- [Hybrid rendering with server routing](https://angular.dev/guide/hybrid-rendering)
-- [Hydration](https://angular.dev/guide/hydration)
-- [Incremental Hydration](https://angular.dev/guide/incremental-hydration)
-
-# CLI
-[Angular CLI Overview](https://angular.dev/tools/cli)
-
-## Testing
-
-- [Testing overview](https://angular.dev/guide/testing)
-- [Testing coverage](https://angular.dev/guide/testing/code-coverage)
-- [Testing services](https://angular.dev/guide/testing/services)
-- [Basics of component testing](https://angular.dev/guide/testing/components-basics)
-- [Component testing scenarios](https://angular.dev/guide/testing/components-scenarios)
-- [Testing attribute directives](https://angular.dev/guide/testing/attribute-directives
-- [Testing pipes](https://angular.dev/guide/testing/pipes
-- [Debugging tests](https://angular.dev/guide/testing/debugging)
-- [Testing utility apis](https://angular.dev/guide/testing/utility-apis)
-- [Component harness overview](https://angular.dev/guide/testing/component-harnesses-overview)
-- [Using component harness in tests](https://angular.dev/guide/testing/using-component-harnesses)
-- [Creating a component harness for your components](https://angular.dev/guide/testing/creating-component-harnesses)
-
-## Animations
-- [Animations your content](https://angular.dev/guide/animations/css)
-- [Route transition animation](https://angular.dev/guide/animations/route-animations)
-- [Migrating to native CSS animations](https://next.angular.dev/guide/animations/migration)
-
-## APIs
-- [API reference](https://angular.dev/api)
-- [CLI command reference](https://angular.dev/cli)
-
-
-## Others
-
-- [Zoneless](https://angular.dev/guide/experimental/zoneless)
-- [Error encyclopedia](https://angular.dev/errors)
-- [Extended diagnostics](https://angular.dev/extended-diagnostics)
-- [Update guide](https://angular.dev/update-guide)
-- [Contribute to Angular](https://github.com/angular/angular/blob/main/CONTRIBUTING.md)
-- [Angular's Roadmap](https://angular.dev/roadmap)
-- [Keeping your projects up-to-date](https://angular.dev/update)
-- [Security](https://angular.dev/best-practices/security)
-- [Internationalization (i18n)](https://angular.dev/guide/i18n)
diff --git a/llms/public/app-description.txt b/llms/public/app-description.txt
new file mode 100644
index 0000000..1857f1d
--- /dev/null
+++ b/llms/public/app-description.txt
@@ -0,0 +1,90 @@
+# app-llm.txt — Copilot / LLM brief for angular.fun (Angular 20+)
+
+## Overview
+**angular.fun** is a blog and code samples site focused on Angular, built with modern Angular features like SSG, SSR, and micro‑frontends. It serves as a resource for learning.
+Actual is a single app with a monolithic structure, but the goal is to refactor it into a multi‑project workspace with clear separation of concerns and deployment strategies.
+App is in the src folder, with a monolithic structure. At the end /src should be empty.
+Current structure is that:
+- `src/app/` contains the app.
+- `src/app/admin` contains the admin part of the app - Pure CSR.
+- `src/app/reader` contains the part of the blog for reader so it's SSG/SSR.
+
+## Goal
+Rebuild **angular.fun** as a clean Angular 20+ multi‑project workspace (no Nx) with three independently deployed apps and shared libraries:
+
+- **projects/web* (public): Hybrid rendering (**SSG for articles**, **SSR for home**; CSR where needed). Deployed to **Google Cloud Run** from source using buildpacks. SEO and Core Web Vitals are top priority. [docs: hybrid rendering, route‑level render mode, Cloud Run deploy from source]
+- **projects/admin**: Pure CSR admin panel. Will be deployed to **Firebase Hosting**.
+- **projects/code-samples-mfe**: Micro‑frontend for heavy examples, loaded lazily from the blog via **Native Federation** (Angular Architects). Deployed to **Firebase Hosting**.
+- **projects/shared**: UI (presentational), pattern (composable UI + injectables), data‑access (REST clients, mappers, Supabase helpers), models.
+
+**State**: **NgRx SignalStore** for all apps.
+
+**Data**: Supabase (Postgres + Auth + Storage). Public content is fetched via **PostgREST REST endpoints** in SSG/SSR to avoid initializing `supabase-js`. Admin/MFE can use `@supabase/supabase-js`. Public assets come from **Supabase Storage CDN**.
+
+**A11y & Performance**: Aim for **100/100/100/100 Lighthouse** on the home page. Enforce with Lighthouse CI in GitHub Actions. Add Playwright + axe accessibility checks.
+
+---
+
+## Projects & folders
+
+**Workspace (Angular CLI):**
+- `projects/web` — public reader app
+- `projects/admin` — admin
+- `projects/code-samples-mfe` — remote
+- `projects/shared` — Library shared between apps
+- `scripts/` — Node scripts (e.g. prerender route generator)
+- `e2e/` — end‑to‑end tests (Playwright)
+- 'llms' - LLMs and Copilot prompts for the project
+- '.github' - GitHub Actions workflows
+
+** Inside each app inside folder should cover architecture from : llms/private/architecture.txt **
+
+## Data access
+
+- **Web**: call **PostgREST** endpoints with the anon key and RLS‑safe policies. Because of issues with `@supabase/supabase-js` in SSR/SSG
+- **Admin/MFE**: use `@supabase/supabase-js` browser client.
+- **Auth**: already implemented with Supabase. When SSR needs session, use `@supabase/ssr` and `createServerClient` with cookies (Cloud Run).
+- **Storage**: serve images from Supabase **CDN** (public bucket). Control cache with versioned filenames or signed URLs if you need cache‑busting.
+
+---
+## Tooling & versions
+
+- **Angular 20+**.
+- **Node.js LTS**: **20.19+** (Angular 20 drops Node 18). Configure Actions runners to Node 20.
+- **Package manager**: `pnpm` recommended (fast, content‑addressable store). Use `actions/setup-node` caching with `cache: 'pnpm'`.
+- **Testing**: Use `@playwright/test` for e2e tests. - implemeted
+- **Linting**: `@angular-eslint` for Angular code, `eslint-plugin-js
+- **boundaries** : eslint-plugin-boundaries
+- **CI/CD**: GitHub Actions.
+
+---
+
+## CI/CD
+
+### Overview
+- Separate jobs for each app. Common `test` job first.
+- **blog-ssg**: build (`ng build` + `ng run blog-ssg:server` + prerender), then **deploy to Cloud Run**. Prefer **Workload Identity Federation** instead of JSON key when possible.
+- **admin-spa** and **code-samples-mfe**: build and **deploy to Firebase Hosting** (each to its own site/target). Use `firebase init hosting:github` to scaffold.
+- **Lighthouse CI**: run on the preview/live URL of the **home page** only. Thresholds set to 1.00 for all categories; fail PRs on regressions.
+- **Accessibility**: Playwright + `@axe-core/playwright` scan of the home page gate in CI.
+
+### Monorepo workflow (sketch)
+See `.github/workflows/ci.yml` below for a consolidated workflow.
+
+---
+
+## i18n roadmap
+
+- Start English‑only. Next locale: **pl**.
+
+---
+
+## SEO decisions
+
+- Generate server‑side meta (title, description, canonical, Open Graph, Twitter). For `og:image`, generate images at publish time and serve from Supabase Storage CDN. Consider adding a small Cloud Run job/Function to generate OG images on demand if needed.
+
+---
+
+## Styling
+- **Tailwind + DaisyUI**. Provide design tokens (colors, spacing, font scale). Set CSS budget limits in `angular.json`. Monitor CSS size in CI.
+
diff --git a/llms/public/guidelines-copilot.md b/llms/public/guidelines-copilot.md
new file mode 100644
index 0000000..71a2b12
--- /dev/null
+++ b/llms/public/guidelines-copilot.md
@@ -0,0 +1,576 @@
+# Persona
+
+You are a dedicated Angular developer who thrives on leveraging the absolute latest features of the framework to build cutting-edge applications. You are currently immersed in Angular v20+, passionately adopting signals for reactive state management, embracing standalone components for streamlined architecture, and utilizing the new control flow for more intuitive template logic. Performance is paramount to you, who constantly seeks to optimize change detection and improve user experience through these modern Angular paradigms. When prompted, assume You are familiar with all the newest APIs and best practices, valuing clean, efficient, and maintainable code.
+
+## Examples
+
+These are modern examples of how to write an Angular 20 component with signals
+
+```ts
+import {ChangeDetectionStrategy, Component, signal} from '@angular/core';
+
+
+@Component({
+ selector: '{{tag-name}}-root',
+ templateUrl: '{{tag-name}}.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class { {
+ ClassName
+}
+}
+{
+protected readonly
+ isServerRunning = signal(true);
+ toggleServerStatus()
+ {
+ this.isServerRunning.update(isServerRunning => !isServerRunning);
+ }
+}
+```
+
+```css
+.container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+
+ button {
+ margin-top: 10px;
+ }
+}
+```
+
+```html
+
+
+ @if (isServerRunning()) {
+ Yes, the server is running
+ } @else {
+ No, the server is not running
+ }
+
+
+```
+
+When you update a component, be sure to put the logic in the ts file, the styles in the css file and the html template in the html file.
+
+## Resources
+
+Here are some links to the essentials for building Angular applications. Use these to get an understanding of how some of the core functionality works
+https://angular.dev/essentials/components
+https://angular.dev/essentials/signals
+https://angular.dev/essentials/templates
+https://angular.dev/essentials/dependency-injection
+
+## Best practices & Style guide
+
+Here are the best practices and the style guide information.
+
+### Coding Style guide
+
+Here is a link to the most recent Angular style guide https://angular.dev/style-guide
+
+### TypeScript Best Practices
+
+- Use strict type checking
+- Prefer type inference when the type is obvious
+- Avoid the `any` type; use `unknown` when type is uncertain
+
+### Angular Best Practices
+
+- Always use standalone components over `NgModules`
+- Do NOT set `standalone: true` inside the `@Component`, `@Directive` and `@Pipe` decorators
+- Use signals for state management
+- Implement lazy loading for feature routes
+- Use `NgOptimizedImage` for all static images.
+- Do NOT use the `@HostBinding` and `@HostListener` decorators. Put host bindings inside the `host` object of the `@Component` or `@Directive` decorator instead
+
+### Components
+
+- Keep components small and focused on a single responsibility
+- Use `input()` signal instead of decorators, learn more here https://angular.dev/guide/components/inputs
+- Use `output()` function instead of decorators, learn more here https://angular.dev/guide/components/outputs
+- Use `computed()` for derived state learn more about signals here https://angular.dev/guide/signals.
+- Set `changeDetection: ChangeDetectionStrategy.OnPush` in `@Component` decorator
+- Prefer inline templates for small components
+- Prefer Reactive forms instead of Template-driven ones
+- Do NOT use `ngClass`, use `class` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings
+- DO NOT use `ngStyle`, use `style` bindings instead, for context: https://angular.dev/guide/templates/binding#css-class-and-style-property-bindings
+
+### State Management
+
+- Use signals for local component state
+- Use `computed()` for derived state
+- Keep state transformations pure and predictable
+- Do NOT use `mutate` on signals, use `update` or `set` instead
+
+### Templates
+
+- Keep templates simple and avoid complex logic
+- Use native control flow (`@if`, `@for`, `@switch`) instead of `*ngIf`, `*ngFor`, `*ngSwitch`
+- Use the async pipe to handle observables
+- Use built in pipes and import pipes when being used in a template, learn more https://angular.dev/guide/templates/pipes#
+
+### Services
+
+- Design services around a single responsibility
+- Use the `providedIn: 'root'` option for singleton services
+- Use the `inject()` function instead of constructor injection
+
+# guidelines-copilot.md — uzupełnienia
+
+## Workspace
+
+- Monorepo Angular CLI (bez Nx). Projekty: `apps/blog-ssg` (SSR/SSG), `apps/admin-spa` (SPA), `apps/code-samples-mfe` (MFE), biblioteki w `libs/`.
+- Architektura według `architecture.txt`: `core/`, `layout/`, `ui/`, `pattern/`, `feature/` (lazy). Granice wymusza `eslint-plugin-boundaries`. Feature’y nie importują się nawzajem.
+
+## Rendering
+
+- Blog: hybrydowo (SSR + SSG). Generuj `routes.txt` lub `getPrerenderParams`. Ciężkie widgety pod `@defer`. W razie problemów z hydracją: `host: { ngSkipHydration: 'true' }`.
+- Admin: wyłącznie CSR.
+- Code‑samples: mikro‑frontend ładowany przez Module/Native Federation; brak prerenderingu.
+
+## State Management — **używaj NgRx SignalStore**
+
+**Zasady ogólne**
+
+- Drobny stan lokalny w komponentach: `signal`, `computed`, `linkedSignal`.
+- Stan domenowy współdzielony: **`@ngrx/signals`**.
+- Transformacje są czyste; **bez mutacji**. Aktualizacje przez `patchState(store, partial)`.
+- Obliczenia wtórne przez `withComputed`. Logika imperatywna i side‑effects w `withMethods` (serwisy HTTP, router itp.).
+- Inicjalizacja/cleanup przez `withHooks({ onInit, onDestroy })`.
+- Dla kolekcji encji używaj `withEntities` i updaterów (`addEntities`, `setEntities`, `updateEntity`, `removeEntity`, itd.).
+- Asynchroniczność:
+ - Proste pobrania: `async/await` + `firstValueFrom` wewnątrz metod store lub
+ - **`rxMethod`** (z `@ngrx/signals/rxjs-interop`) do reakcji na zmiany sygnałów/zdarzeń z operatorskim pipeline (debounce, switchMap, cancelation) albo
+ - **`resource`** (Angular) + `withProps` do odpornego na race conditions ładowania.
+- Interoperacyjność: `toSignal(observable)`, `toObservable(signal)` z `@angular/core/rxjs-interop`.
+
+**Minimalny szablon Store**
+
+```ts
+import {inject, computed} from '@angular/core';
+import {HttpClient} from '@angular/common/http';
+import {
+ signalStore, withState, withComputed, withMethods, withHooks, patchState,
+} from '@ngrx/signals';
+
+interface State {
+ loading: boolean;
+ error: string | null;
+ items: Item[];
+ selectedId: string | null;
+}
+
+const initialState: State = {loading: false, error: null, items: [], selectedId: null};
+
+export const ExampleStore = signalStore(
+ {providedIn: 'root'},
+ withState(initialState),
+ withComputed(({items, selectedId}) => ({
+ selected: computed(() => items().find(i => i.id === selectedId())),
+ count: computed(() => items().length),
+ })),
+ withMethods((store, http = inject(HttpClient)) => ({
+ async load() {
+ patchState(store, {loading: true, error: null});
+ try {
+ const data = await http.get('/api/items').toPromise();
+ patchState(store, {items: data ?? [], loading: false});
+ } catch (e: any) {
+ patchState(store, {loading: false, error: String(e?.message ?? e)});
+ }
+ },
+ select(id: string | null) {
+ patchState(store, {selectedId: id});
+ },
+ upsert(item: Item) {
+ const list = store.items();
+ const i = list.findIndex(x => x.id === item.id);
+ patchState(store, {items: i === -1 ? [...list, item] : list.map(x => x.id === item.id ? item : x)});
+ },
+ })),
+ withHooks({
+ onInit() {/* opcjonalnie */
+ }
+ }),
+);
+```
+
+**rxMethod — reagowanie na zmiany sygnałów**
+
+```ts
+import {rxMethod} from '@ngrx/signals/rxjs-interop';
+import {debounceTime, distinctUntilChanged, switchMap, tap, catchError, of} from 'rxjs';
+
+export const SearchStore = signalStore(
+ withState({query: '', results: [] as Result[], loading: false, error: null as string | null}),
+ withMethods((store, svc = inject(SearchService)) => {
+ const search = rxMethod(pipe(
+ debounceTime(300),
+ distinctUntilChanged(),
+ tap(() => patchState(store, {loading: true, error: null})),
+ switchMap(q => svc.search(q).pipe(
+ tap(results => patchState(store, {results, loading: false})),
+ catchError(err => {
+ patchState(store, {error: String(err), loading: false});
+ return of([]);
+ }),
+ )),
+ ));
+ return {
+ setQuery(q: string) {
+ patchState(store, {query: q});
+ search(q);
+ },
+ };
+ }),
+);
+```
+
+**Entity management**
+
+```ts
+import {withEntities, addEntities, setEntities, updateEntity, removeEntity} from '@ngrx/signals/entities';
+
+type Id = string;
+
+interface Todo {
+ id: Id;
+ title: string;
+ done: boolean;
+}
+
+export const TodosStore = signalStore(
+ withState({loading: false}),
+ withEntities(),
+ withMethods((store) => ({
+ add(todo: Todo) {
+ patchState(store, addEntities(todo));
+ },
+ set(list: Todo[]) {
+ patchState(store, setEntities(list));
+ },
+ toggle(id: Id) {
+ patchState(store, updateEntity({id, changes: (t) => ({...t, done: !t.done})}));
+ },
+ remove(id: Id) {
+ patchState(store, removeEntity({id}));
+ },
+ })),
+);
+```
+
+**Checklist projektowy (ważne)**
+
+- Używaj **signals** w komponentach. Do globalnego stanu i przepływów domenowych używaj **SignalStore**.
+- Nigdy nie mutuj obiektów i tablic w stanie. Zawsze twórz nowe referencje.
+- Oddziel pobieranie danych (metody store/serwisy) od renderowania UI.
+- Efekty uboczne inicjuj w `withMethods` lub `withHooks`, nie w `computed()`.
+- W SSR pamiętaj o bezpiecznym dostępie do `window/document`.
+- Dla tras MFE i ciężkich przykładów wyłącz prerendering i (w razie potrzeby) hydrację.
+
+## Workspace & Projects
+
+- CLI multi‑project workspace (no Nx). Projects:
+ - `apps/blog-ssg` (SSR/SSG hybrid, SEO critical),
+ - `apps/admin-spa` (CSR only),
+ - `apps/code-samples-mfe` (Native Federation remote),
+ - shared libs under `libs/`.
+- All **features are lazy**. Features never import other features directly. Reuse via `ui`, `pattern`, or routing. Enforce boundaries in ESLint. fileciteturn3file0
+
+## Rendering
+
+- Use **route‑level render mode** (Angular v19+) to choose SSR/SSG/CSR per route. Home = SSR, article pages = SSG, others case‑by‑case. citeturn2search2turn0search0
+- Hydration by default. Opt‑out only with `host: { ngSkipHydration: 'true' }` for incompatible widgets. citeturn0search1turn0search16
+- Wrap heavy UI with **`@defer`**; provide placeholders and triggers (`on viewport`, `on idle`, etc.). citeturn0search2turn0search10
+- Optimize images with **`NgOptimizedImage`** and a CDN loader. Prioritize LCP. citeturn2search1turn2search7
+
+## Micro‑frontend
+
+- Prefer **Native Federation** (Angular Architects) for the **code‑samples** remote — minimal setup, integrates with the CLI’s esbuild ApplicationBuilder, future‑proof. Shell: blog. Remote: samples. Load via `loadRemoteModule('samples', './routes')`. citeturn0search7turn0search14
+- If needed, Module Federation is also supported by the same toolkit. citeturn0search6
+
+## Data access (Supabase)
+
+- **Public reader app** should fetch article data over **REST (PostgREST)** during SSG/SSR to avoid supabase-js initialization complexities. Keep requests RLS‑safe or fetch via a server token on Cloud Run. citeturn1search0turn1search5turn1search10
+- For SSR that needs auth, use `@supabase/ssr` and **`createServerClient`** with cookie storage. For browsers (Admin/MFE), use `createBrowserClient`. citeturn1search1turn1search6turn1search16
+
+## **State Management — Use NgRx SignalStore**
+
+### Principles
+
+- Local state: **signals** — `signal`, `computed`, `linkedSignal`. Keep computations pure. fileciteturn3file2turn3file7turn3file3
+- Domain/shared state: **`@ngrx/signals` SignalStore**. Define state via `withState`, derive state via `withComputed`, perform side‑effects in `withMethods`, lifecycle in `withHooks`. Update state immutably with **`patchState`**. citeturn0search3
+- **Do NOT mutate** arrays/objects; always create new references.
+- Async:
+ - Simple fetches: `async/await` + `firstValueFrom`.
+ - Reactive workflows: **`rxMethod`** with RxJS operators.
+ - Interop: `toSignal` / `toObservable` from `@angular/rxjs-interop`. citeturn0search5turn0search20
+- Entities: use `withEntities` + updaters (`addEntities`, `setEntities`, `updateEntity`, `removeEntity`, etc.). Expose selectors via computed signals. citeturn0search4turn0search11
+
+### Store template
+
+```ts
+import {computed, inject} from '@angular/core';
+import {HttpClient} from '@angular/common/http';
+import {
+ signalStore, withState, withComputed, withMethods, withHooks, patchState,
+} from '@ngrx/signals';
+
+interface Item {
+ id: string;
+ title: string;
+}
+
+interface State {
+ loading: boolean;
+ error: string | null;
+ items: Item[];
+ selectedId: string | null;
+}
+
+const initial: State = {loading: false, error: null, items: [], selectedId: null};
+
+export const ItemsStore = signalStore(
+ {providedIn: 'root'},
+ withState(initial),
+ withComputed(({items, selectedId}) => ({
+ count: computed(() => items().length),
+ selected: computed(() => items().find(i => i.id === selectedId())),
+ })),
+ withMethods((store, http = inject(HttpClient)) => ({
+ async load() {
+ patchState(store, {loading: true, error: null});
+ try {
+ const data = await http.get('/api/items').toPromise();
+ patchState(store, {items: data ?? [], loading: false});
+ } catch (e: any) {
+ patchState(store, {loading: false, error: String(e?.message ?? e)});
+ }
+ },
+ select(id: string | null) {
+ patchState(store, {selectedId: id});
+ },
+ })),
+ withHooks({
+ onInit() {/* optional */
+ }
+ }),
+);
+```
+
+Reference: NgRx SignalStore docs. citeturn0search3
+
+### Entities example
+
+```ts
+import {signalStore, withMethods, patchState} from '@ngrx/signals';
+import {withEntities, addEntities, setEntities, updateEntity, removeEntity} from '@ngrx/signals/entities';
+
+type Id = string;
+
+interface Todo {
+ id: Id;
+ title: string;
+ done: boolean;
+}
+
+export const TodosStore = signalStore(
+ withEntities(),
+ withMethods((store) => ({
+ add(todo: Todo) {
+ patchState(store, addEntities(todo));
+ },
+ set(all: Todo[]) {
+ patchState(store, setEntities(all));
+ },
+ toggle(id: Id) {
+ patchState(store, updateEntity({id, changes: (t) => ({...t, done: !t.done})}));
+ },
+ remove(id: Id) {
+ patchState(store, removeEntity({id}));
+ },
+ })),
+);
+```
+
+Reference: NgRx entities plugin docs. citeturn0search4turn0search11
+
+### rxMethod example
+
+```ts
+import {rxMethod} from '@ngrx/signals/rxjs-interop';
+import {debounceTime, distinctUntilChanged, switchMap, tap, catchError, of} from 'rxjs';
+
+export const SearchStore = signalStore(
+ withState({query: '', results: [] as Result[], loading: false, error: null as string | null}),
+ withMethods((store, svc = inject(SearchService)) => {
+ const search = rxMethod(pipe(
+ debounceTime(300),
+ distinctUntilChanged(),
+ tap(() => patchState(store, {loading: true, error: null})),
+ switchMap(q => svc.search(q).pipe(
+ tap(results => patchState(store, {results, loading: false})),
+ catchError(err => {
+ patchState(store, {error: String(err), loading: false});
+ return of([]);
+ }),
+ )),
+ ));
+ return {
+ setQuery(q: string) {
+ patchState(store, {query: q});
+ search(q);
+ },
+ };
+ }),
+);
+```
+
+Reference: NgRx rxMethod docs. citeturn0search5
+
+## Performance & Web Vitals
+
+- Target **100/100/100/100** Lighthouse in CI. Use **Lighthouse CI** GitHub Action and fail PRs on regressions. citeturn1search4turn1search9
+- Use `@defer`, image optimization, lazy features, and skip hydration for problem widgets. Monitor LCP/INP/CLS. citeturn0search2turn2search1
+
+## Accessibility & i18n
+
+- Add a11y linting and automated checks (axe).
+- Plan Angular **i18n** (`$localize`, extraction) and localized builds; later decide which locale routes use SSG/SSR. citeturn2search0turn2search19turn2search2
+
+## Projects
+
+- `apps/blog-ssg` (SSR/SSG hybrid, SEO critical) → Google Cloud Run.
+- `apps/admin-spa` (CSR) → Firebase Hosting.
+- `apps/code-samples-mfe` (Native Federation remote) → Firebase Hosting.
+- `libs/shared/*` for reusable UI, pattern, data-access.
+
+## Rendering
+
+- Use **route‑level render mode** to select SSR/SSG/CSR per route. Home = SSR, `/posts/:slug` = SSG. Hydrate by default; opt‑out with `ngSkipHydration` only when strictly necessary. Use `@defer` to reduce initial JS and improve LCP/INP. Optimize images using `NgOptimizedImage`.
+
+## Micro‑frontend
+
+- Prefer **Native Federation**. It integrates with Angular’s esbuild ApplicationBuilder and is tooling‑agnostic. Blog is the shell; Samples is the remote. Expose routes; load with `loadRemoteModule('samples', './routes')`. Share Angular and RxJS as singletons.
+
+## Supabase
+
+- Public content: fetch via **PostgREST REST** in SSG/SSR (RLS‑safe anon role). Avoid initializing `supabase-js` for prerenders. If SSR needs user context, use `@supabase/ssr` and `createServerClient` with cookie storage.
+- Storage: serve images from **Supabase Storage CDN**. For cache‑busting, prefer versioned filenames or signed URLs.
+
+## State Management — **NgRx SignalStore**
+
+- Local state: `signal`, `computed`, `linkedSignal`.
+- Domain/shared: **SignalStore** with `withState`, `withComputed`, `withMethods`, `withHooks`. Updates via **`patchState`** only. **Do not mutate**.
+- Entities: `withEntities` + updaters (`addEntities`, `setEntities`, `updateEntity`, `removeEntity`, …).
+- RxJS interop: use `rxMethod` for effectful flows; use `toSignal`/`toObservable` bridges when needed.
+
+## Performance & CI
+
+- Target **100/100/100/100** Lighthouse on the home page. Enforce with **Lighthouse CI** in GitHub Actions (fail PRs on regressions).
+- Add **Playwright + axe** accessibility scans on the home page.
+- Node **20.19+** on all runners.
+- Prefer **Workload Identity Federation** for Cloud Run auth when ready.
+
+## i18n
+
+- Plan localized builds (`localize` config) with route prefixes per locale. The dev server supports a **single locale per run**; use `--configuration=`.
+- For SSG, generate `routes..txt` and prerender per locale.
+
+### Api calls
+
+- For SSG/SSR, use this api-url-builder.ts
+- import { ColumnName, RowOf, TableName } from '../types/supabase-helper'
+- This file provides a simple way to build API URLs for Supabase tables using PostgREST conventions.
+
+```ts
+//https://postgrest.org/en/stable/references/api/tables_views.html#operators
+type Op =
+ | 'eq'
+ | 'gt'
+ | 'gte'
+ | 'lt'
+ | 'lte'
+ | 'neq'
+ | 'like'
+ | 'ilike'
+ | 'match'
+ | 'imatch'
+ | 'in'
+ | 'is'
+ | 'isdistinct'
+ | 'fts'
+ | 'plfts'
+ | 'phfts'
+ | 'wfts'
+ | 'cs'
+ | 'cd'
+ | 'ov'
+ | 'sl'
+ | 'sr'
+ | 'nxr'
+ | 'nxl'
+ | 'adj'
+ | 'not'
+ | 'or'
+ | 'and'
+ | 'all'
+ | 'any';
+
+export class UB {
+ private selects = new Set();
+ private filters: string[] = [];
+ private orders: string[] = [];
+ private lim?: number;
+ private off?: number;
+
+ constructor(private readonly table: T) {
+ }
+
+ select(...cols: string[]) {
+ cols.forEach((c) => this.selects.add(c));
+ return this;
+ }
+
+ where>(col: K, op: Op, value: RowOf[K]) {
+ this.filters.push(
+ `${encodeURIComponent(String(col))}=${op}.${encodeURIComponent(String(value))}`,
+ );
+ return this;
+ }
+
+ orderBy>(
+ col: K,
+ dir: 'asc' | 'desc' = 'asc',
+ nulls?: 'first' | 'last',
+ ) {
+ this.orders.push(`${String(col)}.${dir}${nulls ? `.nulls${nulls}` : ''}`);
+ return this;
+ }
+
+ range(from: number, to: number) {
+ this.off = from;
+ this.lim = to - from + 1;
+ return this;
+ }
+
+ build(): string {
+ const p: string[] = [];
+ if (this.selects.size)
+ p.push(`select=${encodeURIComponent([...this.selects].join(','))}`);
+ if (this.filters.length) p.push(...this.filters);
+ if (this.orders.length)
+ p.push(`order=${encodeURIComponent(this.orders.join(','))}`);
+ if (this.lim !== undefined) p.push(`limit=${this.lim}`);
+ if (this.off !== undefined) p.push(`offset=${this.off}`);
+ return `${this.table}?${p.join('&')}`;
+ }
+}
+
+export const createApiUrl = (table: T) => new UB(table);
+```
diff --git a/llms/public/llm-full.txt b/llms/public/llm-full.txt
new file mode 100644
index 0000000..0e43cde
--- /dev/null
+++ b/llms/public/llm-full.txt
@@ -0,0 +1,14281 @@
+
+
+
+Angular is a web framework that empowers developers to build fast, reliable applications.
+
+
+Maintained by a dedicated team at Google, Angular provides a broad suite of tools, APIs, and
+libraries to simplify and streamline your development workflow. Angular gives you
+a solid platform on which to build fast, reliable applications that scale with both the size of
+your team and the size of your codebase.
+
+**Want to see some code?** Jump over to our [Essentials](essentials) for a quick overview of
+what it's like to use Angular, or get started in the [Tutorial](tutorials/learn-angular) if you
+prefer following step-by-step instructions.
+
+## Features that power your development
+## Develop applications faster than ever
+## Ship with confidence
+## Works at any scale
+## Open-source first
+## A thriving community
+Get started with Angular quickly with online starters or locally with your terminal.
+
+## Play Online
+
+If you just want to play around with Angular in your browser without setting up a project, you can use our online sandbox:
+## Set up a new project locally
+
+If you're starting a new project, you'll most likely want to create a local project so that you can use tooling such as Git.
+
+### Prerequisites
+
+- **Node.js** - [v20.11.1 or newer](/reference/versions)
+- **Text editor** - We recommend [Visual Studio Code](https://code.visualstudio.com/)
+- **Terminal** - Required for running Angular CLI commands
+- **Development Tool** - To improve your development workflow, we recommend the [Angular Language Service](/tools/language-service)
+
+### Instructions
+
+The following guide will walk you through setting up a local Angular project.
+
+#### Install Angular CLI
+
+Open a terminal (if you're using [Visual Studio Code](https://code.visualstudio.com/), you can open an [integrated terminal](https://code.visualstudio.com/docs/editor/integrated-terminal)) and run the following command:
+
+```
+// npm
+npm install -g @angular/cli
+```
+```
+// pnpm
+pnpm install -g @angular/cli
+```
+```
+// yarn
+yarn global add @angular/cli
+```
+```
+// bun
+bun install -g @angular/cli
+```
+If you are having issues running this command in Windows or Unix, check out the [CLI docs](/tools/cli/setup-local#install-the-angular-cli) for more info.
+
+#### Create a new project
+
+In your terminal, run the CLI command `ng new` with the desired project name. In the following examples, we'll be using the example project name of `my-first-angular-app`.
+
+```shell
+ng new
+```
+You will be presented with some configuration options for your project. Use the arrow and enter keys to navigate and select which options you desire.
+
+If you don't have any preferences, just hit the enter key to take the default options and continue with the setup.
+
+After you select the configuration options and the CLI runs through the setup, you should see the following message:
+
+```shell
+✔ Packages installed successfully.
+ Successfully initialized git.
+```
+
+At this point, you're now ready to run your project locally!
+
+#### Running your new project locally
+
+In your terminal, switch to your new Angular project.
+
+```shell
+cd my-first-angular-app
+```
+All of your dependencies should be installed at this point (which you can verify by checking for the existent for a `node_modules` folder in your project), so you can start your project by running the command:
+
+```shell
+npm start
+```
+If everything is successful, you should see a similar confirmation message in your terminal:
+
+```shell
+Watch mode enabled. Watching for file changes...
+NOTE: Raw file sizes do not reflect development server per-request transformations.
+ ➜ Local: http://localhost:4200/
+ ➜ press h + enter to show help
+```
+
+And now you can visit the path in `Local` (e.g., `http://localhost:4200`) to see your application. Happy coding! 🎉
+
+### Using AI for Development
+
+To get started with building in your preferred AI powered IDE, [check out Angular prompt rules and best practices](/ai/develop-with-ai).
+
+## Next steps
+
+Now that you've created your Angular project, you can learn more about Angular in our [Essentials guide](/essentials) or choose a topic in our in-depth guides!
+# Angular coding style guide
+
+## Introduction
+
+This guide covers a range of style conventions for Angular application code. These recommendations
+are not required for Angular to work, but instead establish a set of coding practices that promote
+consistency across the Angular ecosystem. A consistent set of practices makes it easier to share
+code and move between projects.
+
+This guide does _not_ cover TypeScript or general coding practices unrelated to Angular. For
+TypeScript, check
+out [Google's TypeScript style guide](https://google.github.io/styleguide/tsguide.html).
+
+### When in doubt, prefer consistency
+
+Whenever you encounter a situation in which these rules contradict the style of a particular file,
+prioritize maintaining consistency within a file. Mixing different style conventions in a single
+file creates more confusion than diverging from the recommendations in this guide.
+
+## Naming
+
+### Separate words in file names with hyphens
+
+Separate words within a file name with hyphens (`-`). For example, a component named `UserProfile`
+has a file name `user-profile.ts`.
+
+### Use the same name for a file's tests with `.spec` at the end
+
+For unit tests, end file names with `.spec.ts`. For example, the unit test file for
+the `UserProfile` component has the file name `user-profile.spec.ts`.
+
+### Match file names to the TypeScript identifier within
+
+File names should generally describe the contents of the code in the file. When the file contains a
+TypeScript class, the file name should reflect that class name. For example, a file containing a
+component named `UserProfile` has the name `user-profile.ts`.
+
+If the file contains more than one primary namable identifier, choose a name that describes the
+common theme to the code within. If the code in a file does not fit within a common theme or feature
+area, consider breaking the code up into different files. Avoid overly generic file names
+like `helpers.ts`, `utils.ts`, or `common.ts`.
+
+### Use the same file name for a component's TypeScript, template, and styles
+
+Components typically consist of one TypeScript file, one template file, and one style file. These
+files should share the same name with different file extensions. For example, a `UserProfile`
+component can have the files `user-profile.ts`, `user-profile.html`, and `user-profile.css`.
+
+If a component has more than one style file, append the name with additional words that describe the
+styles specific to that file. For example, `UserProfile` might have style
+files `user-profile-settings.css` and `user-profile-subscription.css`.
+
+## Project structure
+
+### All the application's code goes in a directory named `src`
+
+All of your Angular UI code (TypeScript, HTML, and styles) should live inside a directory
+named `src`. Code that's not related to UI, such as configuration files or scripts, should live
+outside the `src` directory.
+
+This keeps the root application directory consistent between different Angular projects and creates
+a clear separation between UI code and other code in your project.
+
+### Bootstrap your application in a file named `main.ts` directly inside `src`
+
+The code to start up, or **bootstrap**, an Angular application should always live in a file
+named `main.ts`. This represents the primary entry point to the application.
+
+### Group closely related files together in the same directory
+
+Angular components consist of a TypeScript file and, optionally, a template and one or more style
+files. You should group these together in the same directory.
+
+Unit tests should live in the same directory as the code-under-test. Avoid collecting unrelated
+tests into a single `tests` directory.
+
+### Organize your project by feature areas
+
+Organize your project into subdirectories based on the features of your application or common themes
+to the code in those directories. For example, the project structure for a movie theater site,
+MovieReel, might look like this:
+
+```
+src/
+├─ movie-reel/
+│ ├─ show-times/
+│ │ ├─ film-calendar/
+│ │ ├─ film-details/
+│ ├─ reserve-tickets/
+│ │ ├─ payment-info/
+│ │ ├─ purchase-confirmation/
+```
+
+Avoid creating subdirectories based on the type of code that lives in those directories. For
+example, avoid creating directories like `components`, `directives`, and `services`.
+
+Avoid putting so many files into one directory that it becomes hard to read or navigate. As the
+number of files in a directory grows, consider splitting further into additional sub-directories.
+
+### One concept per file
+
+Prefer focusing source files on a single _concept_. For Angular classes specifically, this usually
+means one component, directive, or service per file. However, it's okay if a file contains more than
+one component or directive if your classes are relatively small and they tie together as part of a
+single concept.
+
+When in doubt, go with the approach that leads to smaller files.
+
+## Dependency injection
+
+### Prefer the `inject` function over constructor parameter injection
+
+Prefer using the `inject` function over injecting constructor parameters. The `inject` function works the same way as constructor parameter injection, but offers several style advantages:
+
+* `inject` is generally more readable, especially when a class injects many dependencies.
+* It's more syntactically straightforward to add comments to injected dependencies
+* `inject` offers better type inference.
+* When targeting ES2022+ with [`useDefineForClassFields`](https://www.typescriptlang.org/tsconfig/#useDefineForClassFields), you can avoid separating field declaration and initialization when fields read on injected dependencies.
+
+[You can refactor existing code to `inject` with an automatic tool](reference/migrations/inject-function).
+
+## Components and directives
+
+### Choosing component selectors
+
+See
+the [Components guide for details on choosing component selectors](guide/components/selectors#choosing-a-selector).
+
+### Naming component and directive members
+
+See the Components guide for details
+on [naming input properties](guide/components/inputs#choosing-input-names)
+and [naming output properties](guide/components/outputs#choosing-event-names).
+
+### Choosing directive selectors
+
+Directives should use the
+same [application-specific prefix](guide/components/selectors#selector-prefixes)
+as your components.
+
+When using an attribute selector for a directive, use a camelCase attribute name. For example, if
+your application is named "MovieReel" and you build a directive that adds a tooltip to an element,
+you might use the selector `[mrTooltip]`.
+
+### Group Angular-specific properties before methods
+
+Components and directives should group Angular-specific properties together, typically near the top
+of the class declaration. This includes injected dependencies, inputs, outputs, and queries. Define
+these and other properties before the class's methods.
+
+This practice makes it easier to find the class's template APIs and dependencies.
+
+### Keep components and directives focused on presentation
+
+Code inside your components and directives should generally relate to the UI shown on the page. For
+code that makes sense on its own, decoupled from the UI, prefer refactoring to other files. For
+example, you can factor form validation rules or data transformations into separate functions or
+classes.
+
+### Avoid overly complex logic in templates
+
+Angular templates are designed to
+accommodate [JavaScript-like expressions](guide/templates/expression-syntax).
+You should take advantage of these expressions to capture relatively straightforward logic directly
+in template expressions.
+
+When the code in a template gets too complex, though, refactor logic into the TypeScript code (typically with a [computed](guide/signals#computed-signals)).
+
+There's no one hard-and-fast rule that determines what constitutes "complex". Use your best
+judgement.
+
+### Use `protected` on class members that are only used by a component's template
+
+A component class's public members intrinsically define a public API that's accessible via
+dependency injection and [queries](guide/components/queries). Prefer `protected`
+access for any members that are meant to be read from the component's template.
+
+```ts
+@Component({
+ ...,
+ template: `
{{ fullName() }}
`,
+})
+export class UserProfile {
+ firstName = input();
+ lastName = input();
+
+// `fullName` is not part of the component's public API, but is used in the template.
+ protected fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
+}
+```
+
+### Use `readonly` on properties that are initialized by Angular
+
+Mark component and directive properties initialized by Angular as `readonly`. This includes
+properties initialized by `input`, `model`, `output`, and queries. The readonly access modifier
+ensures that the value set by Angular is not overwritten.
+
+```ts
+@Component({/* ... */})
+export class UserProfile {
+ readonly userId = input();
+ readonly userSaved = output();
+}
+```
+
+For components and directives that use the decorator-based `@Input`, `@Output`, and query APIs, this
+advice applies to output properties and queries, but not input properties.
+
+```ts
+@Component({/* ... */})
+export class UserProfile {
+ @Output() readonly userSaved = new EventEmitter();
+ @ViewChildren(PaymentMethod) readonly paymentMethods?: QueryList;
+}
+```
+
+### Prefer `class` and `style` over `ngClass` and `ngStyle`
+
+Prefer `class` and `style` bindings over using the [`NgClass`](/api/common/NgClass) and [`NgStyle`](/api/common/NgStyle) directives.
+
+```html
+
+
+
+
+
+
+```
+
+Both `class` and `style` bindings use a more straightforward syntax that aligns closely with
+standard HTML attributes. This makes your templates easier to read and understand, especially for
+developers familiar with basic HTML.
+
+Additionally, the `NgClass` and `NgStyle` directives incur an additional performance cost compared
+to the built-in `class` and `style` binding syntax.
+
+For more details, refer to the [bindings guide](/guide/templates/binding#css-class-and-style-property-bindings)
+
+### Name event handlers for what they _do_, not for the triggering event
+
+Prefer naming event handlers for the action they perform rather than for the triggering event:
+
+```html
+
+
+
+
+
+```
+
+Using meaningful names like this makes it easier to tell what an event does from reading the
+template.
+
+For keyboard events, you can use Angular's key event modifiers with specific handler names:
+
+```html
+
diff --git a/src/index.html b/src/index.html
deleted file mode 100755
index 2d5d429..0000000
--- a/src/index.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
- AngularBlogApp
-
-
-
-
-
-
-
-
-
diff --git a/supabase-password-script-fix.md b/supabase-password-script-fix.md
deleted file mode 100644
index aedbae7..0000000
--- a/supabase-password-script-fix.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Supabase Password Script Fix
-
-## Issue Description
-When running the `npm run users:passwords` command, the following error occurred:
-```
-▶ fetching user IDs…
-jq: error (at :0): Cannot index object with number
-```
-
-## Cause of the Issue
-The error occurred in the `get_id` function of the `scripts/set-passwords.sh` script. The function was using jq to extract the user ID from the JSON response with the command:
-```bash
-jq -r '.[0].id // empty'
-```
-
-This command assumes that the response from the Supabase Auth API is an array, and it tries to access the first element (`.[0]`). However, the error message "Cannot index object with number" indicates that the response is actually an object, not an array, so the `.[0]` indexing fails.
-
-## Solution
-The solution is to modify the jq command to handle both cases: if the response is an array, it will use the original approach; if it's an object, it will try to access the `id` property directly.
-
-### Changes to set-passwords.sh
-```bash
-# Before:
-get_id () { # $1=email
- curl -sS "${HDR[@]:0:2}" "$BASE/users?email=$1" | jq -r '.[0].id // empty'
-}
-
-# After:
-get_id () { # $1=email
- curl -sS "${HDR[@]:0:2}" "$BASE/users?email=$1" | jq -r 'if type == "array" then .[0].id // empty else .id // empty end'
-}
-```
-
-The new jq command uses the `type` function to check if the response is an array. If it is, it accesses the first element's id property as before. If it's an object, it tries to access the id property directly. This makes the script more robust and able to handle different response formats from the Supabase Auth API.
-
-## How to Test
-To test this fix:
-1. Make sure your local Supabase instance is running
-2. Run the script: `npm run users:passwords`
-3. Verify that it successfully sets the passwords for the two users without any errors
diff --git a/tailwind.config.js b/tailwind.config.js
index 8493e7f..4f5192a 100755
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,6 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
- content: ["./src/**/*.{html,ts}"],
+ content: [
+ "./src/**/*.{html,ts}",
+ "./projects/**/*.{html,ts}"
+ ],
daisyui: {
themes: ["light"],
},
@@ -14,5 +17,5 @@ module.exports = {
},
},
},
- plugins: [require("daisyui"), require("@tailwindcss/line-clamp")],
+ plugins: [require("daisyui")],
};
diff --git a/tsconfig.json b/tsconfig.json
index 56b644d..6891e78 100755
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -14,6 +14,17 @@
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
+ "paths": {
+ "shared": [
+ "./projects/shared/src/public-api"
+ ],
+ "shared/models": [
+ "./projects/shared/models/public-api"
+ ],
+ "shared/utils": [
+ "./projects/shared/utils/public-api"
+ ]
+ },
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
@@ -28,5 +39,31 @@
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
- }
+ },
+ "references": [
+ {
+ "path": "./projects/admin/tsconfig.app.json"
+ },
+ {
+ "path": "./projects/admin/tsconfig.spec.json"
+ },
+ {
+ "path": "./projects/web/tsconfig.app.json"
+ },
+ {
+ "path": "./projects/web/tsconfig.spec.json"
+ },
+ {
+ "path": "./projects/shared/tsconfig.lib.json"
+ },
+ {
+ "path": "./projects/shared/tsconfig.spec.json"
+ },
+ {
+ "path": "./projects/code-samples-mfe/tsconfig.app.json"
+ },
+ {
+ "path": "./projects/code-samples-mfe/tsconfig.spec.json"
+ }
+ ]
}