Update ESLint configuration, package dependencies, and improve Prisma integration

- Refactored ESLint configuration for better clarity and organization, including updates to TypeScript rules and Next.js app settings.
- Upgraded package dependencies, including Next.js to version 16.0.8 and Prisma to version 7.1.0, enhancing performance and compatibility.
- Modified Dockerfile for BFF to reflect updated Prisma version and optimize build settings.
- Improved Prisma service to utilize PostgreSQL connection pooling with the new PrismaPg adapter, ensuring better database management.
- Cleaned up TypeScript configuration files for consistency and updated module settings to align with ESNext standards.
- Adjusted pre-commit script to streamline security audits and removed unnecessary linting during development.
This commit is contained in:
barsa 2025-12-10 13:59:41 +09:00
parent f30dcc0608
commit 89b495db1a
18 changed files with 883 additions and 522 deletions

View File

@ -1,21 +1,10 @@
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
# Run type checking
pnpm type-check pnpm type-check
# Linting disabled during active development phase echo "Running security audit..."
# TODO: Re-enable before production release
# pnpm lint
# Quick security check (only fail on high/critical vulnerabilities)
echo "🔒 Running security audit..."
if ! pnpm audit --audit-level=high > /dev/null 2>&1; then if ! pnpm audit --audit-level=high > /dev/null 2>&1; then
echo "" echo "High or critical security vulnerabilities detected!"
echo "⚠️ High or critical security vulnerabilities detected!" echo "Run 'pnpm audit' to see details."
echo "Run 'pnpm audit' to see details and 'pnpm update' to fix."
echo ""
# Uncomment the line below to block commits with vulnerabilities:
# exit 1
fi fi

View File

@ -6,8 +6,8 @@
# ============================================================================= # =============================================================================
ARG NODE_VERSION=22 ARG NODE_VERSION=22
ARG PNPM_VERSION=10.15.0 ARG PNPM_VERSION=10.25.0
ARG PRISMA_VERSION=6.16.0 ARG PRISMA_VERSION=7.1.0
# ============================================================================= # =============================================================================
# Stage 1: Builder # Stage 1: Builder

View File

@ -40,9 +40,9 @@
"@nestjs/passport": "^11.0.5", "@nestjs/passport": "^11.0.5",
"@nestjs/platform-express": "^11.1.9", "@nestjs/platform-express": "^11.1.9",
"@nestjs/throttler": "^6.5.0", "@nestjs/throttler": "^6.5.0",
"@prisma/client": "^6.19.0", "@prisma/adapter-pg": "^7.1.0",
"@prisma/client": "^7.1.0",
"@sendgrid/mail": "^8.1.6", "@sendgrid/mail": "^8.1.6",
"@types/ssh2-sftp-client": "^9.0.6",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"bullmq": "^5.65.1", "bullmq": "^5.65.1",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
@ -57,6 +57,7 @@
"passport": "^0.7.0", "passport": "^0.7.0",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"pg": "^8.16.3",
"pino": "^10.1.0", "pino": "^10.1.0",
"pino-http": "^11.0.0", "pino-http": "^11.0.0",
"pino-pretty": "^13.1.3", "pino-pretty": "^13.1.3",
@ -80,18 +81,18 @@
"@types/node": "^24.10.2", "@types/node": "^24.10.2",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38", "@types/passport-local": "^1.0.38",
"@types/pg": "^8.15.6",
"@types/ssh2-sftp-client": "^9.0.6",
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"jest": "^30.2.0", "jest": "^30.2.0",
"prisma": "^6.19.0", "prisma": "^7.1.0",
"source-map-support": "^0.5.21", "source-map-support": "^0.5.21",
"supertest": "^7.1.4", "supertest": "^7.1.4",
"ts-jest": "^29.4.6", "ts-jest": "^29.4.6",
"ts-loader": "^9.5.4", "ts-loader": "^9.5.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"ttypescript": "^1.5.15", "typescript": "^5.9.3"
"typescript": "^5.9.3",
"typescript-transform-paths": "^3.5.5"
}, },
"jest": { "jest": {
"moduleFileExtensions": [ "moduleFileExtensions": [

View File

@ -0,0 +1,29 @@
import { defineConfig } from "prisma";
import { PrismaPg } from "@prisma/adapter-pg";
import { Pool } from "pg";
/**
* Prisma 7 Configuration
*
* This configuration file is required for Prisma 7+ where the datasource URL
* is no longer specified in schema.prisma. Instead, connection configuration
* is provided here for migrations and in the PrismaClient constructor for runtime.
*
* @see https://pris.ly/d/config-datasource
* @see https://pris.ly/d/prisma7-client-config
*/
export default defineConfig({
earlyAccess: true,
schema: "./schema.prisma",
migrate: {
adapter: async () => {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL environment variable is required for migrations");
}
const pool = new Pool({ connectionString });
return new PrismaPg(pool);
},
},
});

View File

@ -15,7 +15,8 @@ generator client {
datasource db { datasource db {
provider = "postgresql" provider = "postgresql"
url = env("DATABASE_URL") // Note: Prisma 7+ requires connection URL to be passed via adapter in PrismaClient constructor
// See prisma.config.ts for migration configuration
} }
model User { model User {

View File

@ -1,13 +1,54 @@
import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common"; import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from "@nestjs/common";
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
import { Pool } from "pg";
import { PrismaPg } from "@prisma/adapter-pg";
/**
* Prisma Service
*
* Prisma 7+ requires passing an adapter for database connections instead of
* specifying the URL in the schema file. This service creates a PostgreSQL
* connection pool and passes it to the PrismaClient via the PrismaPg adapter.
*
* @see https://pris.ly/d/prisma7-client-config
*/
@Injectable() @Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(PrismaService.name);
private readonly pool: Pool;
constructor() {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error("DATABASE_URL environment variable is required");
}
// Create a connection pool for PostgreSQL
const pool = new Pool({
connectionString,
// Connection pool settings optimized for NestJS
max: 10, // Maximum number of clients in the pool
idleTimeoutMillis: 30000, // Close idle clients after 30 seconds
connectionTimeoutMillis: 5000, // Return an error after 5 seconds if connection fails
});
// Create the Prisma PostgreSQL adapter
const adapter = new PrismaPg(pool);
// Initialize PrismaClient with the adapter
super({ adapter });
this.pool = pool;
}
async onModuleInit() { async onModuleInit() {
await this.$connect(); await this.$connect();
this.logger.log("Database connection established");
} }
async onModuleDestroy() { async onModuleDestroy() {
await this.$disconnect(); await this.$disconnect();
await this.pool.end();
this.logger.log("Database connection closed");
} }
} }

View File

@ -1,39 +1,25 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
// NestJS specific settings "module": "CommonJS",
"moduleResolution": "Node",
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"strictPropertyInitialization": false, "strictPropertyInitialization": false,
// Path mappings for clean imports
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["src/*"], "@/*": ["src/*"],
"@bff/core/*": ["src/core/*"], "@bff/*": ["src/*"]
"@bff/infra/*": ["src/infra/*"],
"@bff/modules/*": ["src/modules/*"],
"@bff/integrations/*": ["src/integrations/*"],
"@customer-portal/validation": ["../../packages/validation/dist/index"],
"@customer-portal/validation/*": ["../../packages/validation/dist/*"]
}, },
"rootDirs": [
"src",
"../../packages/validation/dist",
"../../packages/validation/src"
],
// Type checking
"noEmit": true, "noEmit": true,
"types": ["node"], "types": ["node", "jest"]
"typeRoots": ["./node_modules/@types"]
}, },
"ts-node": { "ts-node": {
"transpileOnly": true, "transpileOnly": true,
"compilerOptions": { "compilerOptions": {
"module": "commonjs" "module": "CommonJS"
} }
}, },
"include": ["src/**/*", "scripts/**/*", "test/**/*"], "include": ["src/**/*", "test/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist", "prisma"]
} }

View File

@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" /> import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@ -1,111 +1,78 @@
/* eslint-env node */ /* eslint-env node */
import bundleAnalyzer from "@next/bundle-analyzer"; import bundleAnalyzer from "@next/bundle-analyzer";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const workspaceRoot = path.resolve(__dirname, "..", "..");
const withBundleAnalyzer = bundleAnalyzer({ const withBundleAnalyzer = bundleAnalyzer({
enabled: process.env.ANALYZE === "true", enabled: process.env.ANALYZE === "true",
}); });
import path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const nextConfig = { const nextConfig = {
// Enable standalone output only for production deployment
output: process.env.NODE_ENV === "production" ? "standalone" : undefined, output: process.env.NODE_ENV === "production" ? "standalone" : undefined,
// Ensure workspace packages are transpiled correctly
transpilePackages: ["@customer-portal/domain", "@customer-portal/validation"],
// Tell Next to NOT bundle these server-only libs
serverExternalPackages: [ serverExternalPackages: [
"pino", "pino",
"pino-pretty", "pino-pretty",
"pino-abstract-transport", "pino-abstract-transport",
"thread-stream", "thread-stream",
"sonic-boom", "sonic-boom",
// Avoid flaky vendor-chunk resolution during dev for small utils
"tailwind-merge", "tailwind-merge",
], ],
// Turbopack configuration (Next.js 15.5+) turbopack: {
// Note: public turbopack options are limited; aliasing is handled via tsconfig/webpack resolutions resolveAlias: {
"@customer-portal/domain": path.join(workspaceRoot, "packages/domain/dist"),
},
},
webpack(config) {
config.resolve.alias = {
...config.resolve.alias,
"@customer-portal/domain": path.join(workspaceRoot, "packages/domain/dist"),
};
return config;
},
// Environment variables validation
env: { env: {
NEXT_PUBLIC_API_BASE: process.env.NEXT_PUBLIC_API_BASE, NEXT_PUBLIC_API_BASE: process.env.NEXT_PUBLIC_API_BASE,
NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME, NEXT_PUBLIC_APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION, NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION,
}, },
// Image optimization
images: { images: {
remotePatterns: [ remotePatterns: [{ protocol: "https", hostname: "**" }],
{
protocol: "https",
hostname: "**",
},
],
}, },
// Disable ESLint blocking during production builds to avoid CI/CD failures
eslint: {
ignoreDuringBuilds: true,
},
// API rewrites for development - proxy /api/* to BFF
// This is the standard Next.js pattern that mirrors production (nginx proxy)
async rewrites() { async rewrites() {
// Only apply rewrites in development; production uses nginx
if (process.env.NODE_ENV !== "production") { if (process.env.NODE_ENV !== "production") {
return [ return [{ source: "/api/:path*", destination: "http://localhost:4000/api/:path*" }];
{
source: "/api/:path*",
destination: "http://localhost:4000/api/:path*",
},
];
} }
return []; return [];
}, },
// Security headers
async headers() { async headers() {
const isDev = process.env.NODE_ENV === "development";
return [ return [
{ {
// Apply security headers to all routes
source: "/(.*)", source: "/(.*)",
headers: [ headers: [
{ { key: "X-Frame-Options", value: "DENY" },
key: "X-Frame-Options", { key: "X-Content-Type-Options", value: "nosniff" },
value: "DENY", { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
}, { key: "X-XSS-Protection", value: "1; mode=block" },
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "X-XSS-Protection",
value: "1; mode=block",
},
// Content Security Policy - allows Next.js inline scripts/styles
{ {
key: "Content-Security-Policy", key: "Content-Security-Policy",
value: [ value: [
"default-src 'self'", "default-src 'self'",
// Next.js requires unsafe-inline for hydration scripts
"script-src 'self' 'unsafe-inline' 'unsafe-eval'", "script-src 'self' 'unsafe-inline' 'unsafe-eval'",
// Next.js/Tailwind requires unsafe-inline for styles
"style-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:", "img-src 'self' data: https:",
"font-src 'self' data:", "font-src 'self' data:",
// Allow API connections (include localhost for development) `connect-src 'self' https:${isDev ? " http://localhost:*" : ""}`,
// Allow localhost in development for API calls to BFF
`connect-src 'self' https: ${process.env.NODE_ENV === "development" ? "http://localhost:*" : ""}`,
"frame-ancestors 'none'", "frame-ancestors 'none'",
].join("; "), ].join("; "),
}, },
@ -114,44 +81,15 @@ const nextConfig = {
]; ];
}, },
// Production optimizations
compiler: { compiler: {
// Remove console.logs in production
removeConsole: process.env.NODE_ENV === "production", removeConsole: process.env.NODE_ENV === "production",
}, },
// Experimental flags
experimental: { experimental: {
externalDir: true, optimizePackageImports: ["@heroicons/react", "@tanstack/react-query"],
optimizePackageImports: ["@heroicons/react", "lucide-react", "@tanstack/react-query"],
}, },
webpack(config) {
const workspaceRoot = path.resolve(__dirname, "..", "..");
config.resolve.alias = {
...config.resolve.alias,
"@customer-portal/domain": path.join(workspaceRoot, "packages/domain"),
"@customer-portal/validation": path.join(workspaceRoot, "packages/validation/src"),
};
const preferredExtensions = [".ts", ".tsx", ".mts", ".cts"];
const existingExtensions = config.resolve.extensions || [];
config.resolve.extensions = [...new Set([...preferredExtensions, ...existingExtensions])];
config.resolve.extensionAlias = {
...(config.resolve.extensionAlias || {}),
".js": [".ts", ".tsx", ".js"],
".mjs": [".mts", ".ts", ".tsx", ".mjs"],
};
config.module.rules.push({
test: /packages\/domain\/.*\.js$/,
type: "javascript/esm",
});
return config;
},
// Keep type checking enabled; monorepo paths provide types
typescript: { ignoreBuildErrors: false }, typescript: { ignoreBuildErrors: false },
// Prefer Turbopack; no custom webpack override needed
}; };
export default withBundleAnalyzer(nextConfig); export default withBundleAnalyzer(nextConfig);

View File

@ -6,7 +6,7 @@
"predev": "node ./scripts/dev-prep.mjs", "predev": "node ./scripts/dev-prep.mjs",
"dev": "next dev -p ${NEXT_PORT:-3000}", "dev": "next dev -p ${NEXT_PORT:-3000}",
"build": "next build", "build": "next build",
"build:turbo": "next build --turbopack", "build:webpack": "next build --webpack",
"build:analyze": "ANALYZE=true next build", "build:analyze": "ANALYZE=true next build",
"analyze": "npm run build:analyze", "analyze": "npm run build:analyze",
"start": "next start -p ${NEXT_PORT:-3000}", "start": "next start -p ${NEXT_PORT:-3000}",
@ -25,7 +25,7 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"next": "15.5.7", "next": "16.0.8",
"react": "19.2.1", "react": "19.2.1",
"react-dom": "19.2.1", "react-dom": "19.2.1",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
@ -35,7 +35,7 @@
"zustand": "^5.0.9" "zustand": "^5.0.9"
}, },
"devDependencies": { "devDependencies": {
"@next/bundle-analyzer": "^15.5.7", "@next/bundle-analyzer": "^16.0.8",
"@tailwindcss/postcss": "^4.1.17", "@tailwindcss/postcss": "^4.1.17",
"@types/node": "^24.10.2", "@types/node": "^24.10.2",
"@types/react": "^19.2.7", "@types/react": "^19.2.7",

View File

@ -1,27 +1,17 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"target": "ES2022",
"lib": ["ES2024", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "preserve", "jsx": "preserve",
"noEmit": true, "noEmit": true,
"moduleResolution": "node",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"plugins": [{ "name": "next" }], "plugins": [{ "name": "next" }],
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./src/*"], "@/*": ["./src/*"]
"@/components/*": ["./src/components/*"], }
"@/core/*": ["./src/core/*"],
"@/features/*": ["./src/features/*"],
"@/shared/*": ["./src/shared/*"],
"@/styles/*": ["./src/styles/*"],
"@/types/*": ["./src/types/*"],
"@/lib/*": ["./src/lib/*"],
"@customer-portal/domain": ["../../packages/domain/index.ts"],
"@customer-portal/domain/*": ["../../packages/domain/*"],
"@customer-portal/validation": ["../../packages/validation/src"],
"@customer-portal/validation/*": ["../../packages/validation/src/*"]
},
"allowJs": false
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

View File

@ -1,13 +1,12 @@
/* eslint-env node */ /* eslint-env node */
import process from "node:process"; import process from "node:process";
import path from "node:path";
import js from "@eslint/js"; import js from "@eslint/js";
import tseslint from "typescript-eslint"; import tseslint from "typescript-eslint";
import prettier from "eslint-plugin-prettier"; import prettier from "eslint-plugin-prettier";
import { FlatCompat } from "@eslint/eslintrc"; import { FlatCompat } from "@eslint/eslintrc";
import path from "node:path";
import globals from "globals"; import globals from "globals";
// Use FlatCompat to consume Next.js' legacy shareable configs under apps/portal
const compat = new FlatCompat({ baseDirectory: path.resolve("apps/portal") }); const compat = new FlatCompat({ baseDirectory: path.resolve("apps/portal") });
export default [ export default [
@ -23,35 +22,28 @@ export default [
], ],
}, },
// Base JS recommendations for all files // Base JS recommendations
js.configs.recommended, js.configs.recommended,
// Prettier integration (warn-only) // Prettier integration
{ {
plugins: { prettier }, plugins: { prettier },
rules: { rules: { "prettier/prettier": "warn" },
"prettier/prettier": "warn",
},
}, },
// TypeScript (type-checked) for TS files only // TypeScript type-checked rules for all TS files
...tseslint.configs.recommendedTypeChecked.map(config => ({ ...tseslint.configs.recommendedTypeChecked.map((config) => ({
...config, ...config,
files: ["**/*.ts", "**/*.tsx"], files: ["**/*.ts", "**/*.tsx"],
languageOptions: { languageOptions: {
...(config.languageOptions || {}), ...(config.languageOptions || {}),
globals: { globals: { ...globals.node },
...globals.node,
},
}, },
})), })),
// Backend & domain packages
{ {
files: [ files: ["apps/bff/**/*.ts", "packages/domain/**/*.ts"],
"apps/bff/**/*.ts",
"packages/domain/**/*.ts",
"packages/logging/**/*.ts",
"packages/validation/**/*.ts",
],
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
projectService: true, projectService: true,
@ -60,17 +52,14 @@ export default [
}, },
rules: { rules: {
"@typescript-eslint/consistent-type-imports": "error", "@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }],
"warn",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
"no-console": ["warn", { allow: ["warn", "error"] }], "no-console": ["warn", { allow: ["warn", "error"] }],
}, },
}, },
// Enforce consistent strict rules across shared as well // Strict rules for shared packages
{ {
files: ["packages/**/*.{ts,tsx}"], files: ["packages/**/*.ts"],
rules: { rules: {
"@typescript-eslint/no-redundant-type-constituents": "error", "@typescript-eslint/no-redundant-type-constituents": "error",
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
@ -80,15 +69,17 @@ export default [
}, },
}, },
// Next.js app: apply Next's recommended config; TS rules only on TS files // Next.js app config
...compat ...compat.extends("next/core-web-vitals").map((config) => ({
.extends("next/core-web-vitals") ...config,
.map(config => ({ ...config, files: ["apps/portal/**/*.{js,jsx,ts,tsx}"] })), files: ["apps/portal/**/*.{js,jsx,ts,tsx}"],
...compat })),
.extends("next/typescript") ...compat.extends("next/typescript").map((config) => ({
.map(config => ({ ...config, files: ["apps/portal/**/*.{ts,tsx}"] })), ...config,
files: ["apps/portal/**/*.{ts,tsx}"],
})),
// Ensure type-aware rules in portal have parser services // Portal type-aware rules
{ {
files: ["apps/portal/**/*.{ts,tsx}"], files: ["apps/portal/**/*.{ts,tsx}"],
languageOptions: { languageOptions: {
@ -102,41 +93,31 @@ export default [
}, },
}, },
// Authenticated pages should rely on the shared route-group layout at (authenticated)/layout.tsx. // Prevent hard navigation in authenticated pages
{ {
files: ["apps/portal/src/app/(authenticated)/**/*.{ts,tsx}"], files: ["apps/portal/src/app/(authenticated)/**/*.{ts,tsx}"],
rules: { rules: {
// Prefer Next.js <Link> and router, forbid window/location hard reload in portal pages
"no-restricted-syntax": [ "no-restricted-syntax": [
"error", "error",
{ {
selector: "MemberExpression[object.name='window'][property.name='location']", selector: "MemberExpression[object.name='window'][property.name='location']",
message: "Use next/link or useRouter for navigation, not window.location.", message: "Use next/link or useRouter for navigation.",
},
{
selector:
"MemberExpression[object.name='location'][property.name=/^(href|assign|replace)$/]",
message: "Use next/link or useRouter for navigation, not location.*.",
}, },
], ],
}, },
}, },
// Allow the shared layout file itself to import the layout component
// Exceptions for specific files
{ {
files: ["apps/portal/src/app/(authenticated)/layout.tsx"], files: ["apps/portal/src/app/(authenticated)/layout.tsx"],
rules: { rules: { "no-restricted-imports": "off" },
"no-restricted-imports": "off",
},
}, },
// Allow controlled window.location usage for invoice SSO download
{ {
files: ["apps/portal/src/app/(authenticated)/billing/invoices/[id]/page.tsx"], files: ["apps/portal/src/app/(authenticated)/billing/invoices/[id]/page.tsx"],
rules: { rules: { "no-restricted-syntax": "off" },
"no-restricted-syntax": "off",
},
}, },
// Enforce layered type system architecture // Enforce domain imports architecture
{ {
files: ["apps/portal/src/**/*.{ts,tsx}", "apps/bff/src/**/*.ts"], files: ["apps/portal/src/**/*.{ts,tsx}", "apps/bff/src/**/*.ts"],
rules: { rules: {
@ -146,52 +127,15 @@ export default [
patterns: [ patterns: [
{ {
group: ["@customer-portal/domain/**/src/**"], group: ["@customer-portal/domain/**/src/**"],
message: message: "Import from @customer-portal/domain/<module> instead of internals.",
"Don't import from domain package internals. Use @customer-portal/domain/<domain> or its Providers namespace instead.",
},
{
group: ["**/utils/ui-state*"],
message:
"ui-state.ts has been removed. Use patterns from @customer-portal/domain instead.",
},
{
group: ["@/types"],
message:
"Avoid importing from @/types. Import types directly from @customer-portal/domain/<domain> or define locally.",
}, },
], ],
}, },
], ],
// Prevent defining deprecated type patterns
"no-restricted-syntax": [
"error",
{
selector:
"TSInterfaceDeclaration[id.name=/^(LegacyAsyncState|PaginatedState|FilteredState)$/]",
message:
"These legacy state types are deprecated. Use AsyncState, PaginatedAsyncState, or FilterState from @customer-portal/domain instead.",
},
{
selector:
"TSTypeAliasDeclaration[id.name=/^(LegacyAsyncState|PaginatedState|FilteredState)$/]",
message:
"These legacy state types are deprecated. Use AsyncState, PaginatedAsyncState, or FilterState from @customer-portal/domain instead.",
},
],
}, },
}, },
// Node globals for Next config file // BFF strict type safety
{
files: ["apps/portal/next.config.mjs"],
languageOptions: {
globals: {
...globals.node,
},
},
},
// BFF: strict rules enforced + prevent domain type duplication
{ {
files: ["apps/bff/**/*.ts"], files: ["apps/bff/**/*.ts"],
rules: { rules: {
@ -203,40 +147,14 @@ export default [
"@typescript-eslint/no-unsafe-argument": "error", "@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/require-await": "error", "@typescript-eslint/require-await": "error",
"@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-floating-promises": "error",
"no-restricted-syntax": [
"error",
{
selector:
"TSInterfaceDeclaration[id.name=/^(Invoice|InvoiceItem|Subscription|PaymentMethod|SimDetails)$/]",
message:
"Don't re-declare domain types in application code. Import from @customer-portal/domain/<domain> instead.",
},
{
selector:
"TSTypeAliasDeclaration[id.name=/^(Invoice|InvoiceItem|Subscription|PaymentMethod|SimDetails)$/]",
message:
"Don't re-declare domain types in application code. Import from @customer-portal/domain/<domain> instead.",
},
],
}, },
}, },
// Integration packages: must use domain providers and avoid legacy packages // Node globals for config files
{ {
files: ["packages/integrations/**/src/**/*.ts"], files: ["*.config.mjs", "apps/portal/next.config.mjs"],
rules: { languageOptions: {
"no-restricted-imports": [ globals: { ...globals.node },
"error",
{
patterns: [
{
group: ["@customer-portal/contracts", "@customer-portal/contracts/*", "@customer-portal/schemas", "@customer-portal/schemas/*"],
message:
"Legacy contracts/schemas packages have been removed. Import from @customer-portal/domain/<domain>/providers instead.",
},
],
},
],
}, },
}, },
]; ];

View File

@ -9,10 +9,9 @@
}, },
"packageManager": "pnpm@10.25.0+sha512.5e82639027af37cf832061bcc6d639c219634488e0f2baebe785028a793de7b525ffcd3f7ff574f5e9860654e098fe852ba8ac5dd5cefe1767d23a020a92f501", "packageManager": "pnpm@10.25.0+sha512.5e82639027af37cf832061bcc6d639c219634488e0f2baebe785028a793de7b525ffcd3f7ff574f5e9860654e098fe852ba8ac5dd5cefe1767d23a020a92f501",
"scripts": { "scripts": {
"predev": "pnpm --filter @customer-portal/domain build",
"dev": "./scripts/dev/manage.sh apps", "dev": "./scripts/dev/manage.sh apps",
"dev:all": "pnpm --parallel --filter @customer-portal/domain --filter @customer-portal/portal --filter @customer-portal/bff run dev", "dev:all": "pnpm --filter @customer-portal/domain build && pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run dev",
"build": "pnpm --recursive run build", "build": "pnpm --filter @customer-portal/domain build && pnpm --recursive --filter=!@customer-portal/domain run build",
"start": "pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run start", "start": "pnpm --parallel --filter @customer-portal/portal --filter @customer-portal/bff run start",
"test": "pnpm --recursive run test", "test": "pnpm --recursive run test",
"lint": "pnpm --recursive run lint", "lint": "pnpm --recursive run lint",
@ -20,23 +19,14 @@
"format": "prettier -w .", "format": "prettier -w .",
"format:check": "prettier -c .", "format:check": "prettier -c .",
"prepare": "husky", "prepare": "husky",
"type-check": "pnpm type-check:packages && pnpm type-check:apps", "type-check": "pnpm --filter @customer-portal/domain run type-check && pnpm --filter @customer-portal/bff --filter @customer-portal/portal run type-check",
"type-check:workspace": "tsc -b --noEmit",
"type-check:packages": "pnpm --workspace-concurrency=1 --filter @customer-portal/domain --filter @customer-portal/validation --filter @customer-portal/logging run type-check",
"type-check:apps": "pnpm --workspace-concurrency=1 --filter @customer-portal/bff --filter @customer-portal/portal run type-check",
"clean": "pnpm --recursive run clean", "clean": "pnpm --recursive run clean",
"dev:start": "./scripts/dev/manage.sh start", "dev:start": "./scripts/dev/manage.sh start",
"dev:stop": "./scripts/dev/manage.sh stop", "dev:stop": "./scripts/dev/manage.sh stop",
"dev:restart": "./scripts/dev/manage.sh restart", "dev:restart": "./scripts/dev/manage.sh restart",
"analyze": "pnpm --filter @customer-portal/portal run analyze",
"bundle-analyze": "./scripts/bundle-analyze.sh",
"dev:tools": "./scripts/dev/manage.sh tools", "dev:tools": "./scripts/dev/manage.sh tools",
"dev:apps": "./scripts/dev/manage.sh apps",
"dev:logs": "./scripts/dev/manage.sh logs", "dev:logs": "./scripts/dev/manage.sh logs",
"dev:status": "./scripts/dev/manage.sh status", "dev:status": "./scripts/dev/manage.sh status",
"dev:migrate": "./scripts/dev/manage.sh migrate",
"dev:studio": "./scripts/dev/manage.sh studio",
"dev:reset": "./scripts/dev/manage.sh reset",
"prod:deploy": "./scripts/prod/manage.sh deploy", "prod:deploy": "./scripts/prod/manage.sh deploy",
"prod:start": "./scripts/prod/manage.sh start", "prod:start": "./scripts/prod/manage.sh start",
"prod:stop": "./scripts/prod/manage.sh stop", "prod:stop": "./scripts/prod/manage.sh stop",
@ -48,38 +38,29 @@
"prod:backup": "./scripts/prod/manage.sh backup", "prod:backup": "./scripts/prod/manage.sh backup",
"prod:cleanup": "./scripts/prod/manage.sh cleanup", "prod:cleanup": "./scripts/prod/manage.sh cleanup",
"db:migrate": "pnpm --filter @customer-portal/bff run db:migrate", "db:migrate": "pnpm --filter @customer-portal/bff run db:migrate",
"db:generate": "pnpm --filter @customer-portal/bff run db:generate",
"db:studio": "pnpm --filter @customer-portal/bff run db:studio", "db:studio": "pnpm --filter @customer-portal/bff run db:studio",
"db:reset": "pnpm --filter @customer-portal/bff run db:reset", "db:reset": "pnpm --filter @customer-portal/bff run db:reset",
"update:check": "pnpm outdated --recursive",
"update:all": "pnpm update --recursive --latest && pnpm audit && pnpm type-check",
"update:safe": "pnpm update --recursive && pnpm audit && pnpm type-check",
"security:audit": "pnpm audit", "security:audit": "pnpm audit",
"security:audit-fix": "pnpm audit --fix || pnpm update --recursive && pnpm audit",
"security:check": "pnpm audit --audit-level=high", "security:check": "pnpm audit --audit-level=high",
"dev:watch": "pnpm --parallel --filter @customer-portal/domain --filter @customer-portal/portal --filter @customer-portal/bff run dev", "update:check": "pnpm outdated --recursive",
"plesk:images": "bash ./scripts/plesk/build-images.sh", "update:safe": "pnpm update --recursive && pnpm audit && pnpm type-check",
"postinstall": "husky install || true" "analyze": "pnpm --filter @customer-portal/portal run analyze",
"plesk:images": "bash ./scripts/plesk/build-images.sh"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3.3.3", "@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.1", "@eslint/js": "^9.39.1",
"@types/node": "^24.10.2", "@types/node": "^24.10.2",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-config-next": "15.5.0", "eslint-config-next": "16.0.8",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.4",
"globals": "^16.5.0", "globals": "^16.5.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"pino": "^10.1.0",
"prettier": "^3.7.4", "prettier": "^3.7.4",
"sharp": "^0.34.5",
"tsx": "^4.21.0", "tsx": "^4.21.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.49.0", "typescript-eslint": "^8.49.0"
"zod": "^4.1.13"
},
"dependencies": {
"@types/ssh2-sftp-client": "^9.0.6",
"ssh2-sftp-client": "^12.0.1"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {

View File

@ -1,7 +1,7 @@
{ {
"name": "@customer-portal/domain", "name": "@customer-portal/domain",
"version": "1.0.0", "version": "1.0.0",
"type": "commonjs", "type": "module",
"description": "Unified domain layer with contracts, schemas, and provider mappers", "description": "Unified domain layer with contracts, schemas, and provider mappers",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
@ -11,34 +11,118 @@
"dist" "dist"
], ],
"exports": { "exports": {
".": "./dist/index.js", ".": {
"./auth": "./dist/auth/index.js", "import": "./dist/index.js",
"./auth/*": "./dist/auth/*.js", "types": "./dist/index.d.ts"
"./billing": "./dist/billing/index.js", },
"./billing/*": "./dist/billing/*.js", "./auth": {
"./catalog": "./dist/catalog/index.js", "import": "./dist/auth/index.js",
"./catalog/*": "./dist/catalog/*.js", "types": "./dist/auth/index.d.ts"
"./common": "./dist/common/index.js", },
"./common/*": "./dist/common/*.js", "./auth/*": {
"./customer": "./dist/customer/index.js", "import": "./dist/auth/*.js",
"./customer/*": "./dist/customer/*.js", "types": "./dist/auth/*.d.ts"
"./dashboard": "./dist/dashboard/index.js", },
"./dashboard/*": "./dist/dashboard/*.js", "./billing": {
"./mappings": "./dist/mappings/index.js", "import": "./dist/billing/index.js",
"./mappings/*": "./dist/mappings/*.js", "types": "./dist/billing/index.d.ts"
"./orders": "./dist/orders/index.js", },
"./orders/*": "./dist/orders/*.js", "./billing/*": {
"./payments": "./dist/payments/index.js", "import": "./dist/billing/*.js",
"./payments/*": "./dist/payments/*.js", "types": "./dist/billing/*.d.ts"
"./sim": "./dist/sim/index.js", },
"./sim/*": "./dist/sim/*.js", "./catalog": {
"./sim/providers/freebit": "./dist/sim/providers/freebit/index.js", "import": "./dist/catalog/index.js",
"./subscriptions": "./dist/subscriptions/index.js", "types": "./dist/catalog/index.d.ts"
"./subscriptions/*": "./dist/subscriptions/*.js", },
"./support": "./dist/support/index.js", "./catalog/*": {
"./support/*": "./dist/support/*.js", "import": "./dist/catalog/*.js",
"./toolkit": "./dist/toolkit/index.js", "types": "./dist/catalog/*.d.ts"
"./toolkit/*": "./dist/toolkit/*.js" },
"./common": {
"import": "./dist/common/index.js",
"types": "./dist/common/index.d.ts"
},
"./common/*": {
"import": "./dist/common/*.js",
"types": "./dist/common/*.d.ts"
},
"./customer": {
"import": "./dist/customer/index.js",
"types": "./dist/customer/index.d.ts"
},
"./customer/*": {
"import": "./dist/customer/*.js",
"types": "./dist/customer/*.d.ts"
},
"./dashboard": {
"import": "./dist/dashboard/index.js",
"types": "./dist/dashboard/index.d.ts"
},
"./dashboard/*": {
"import": "./dist/dashboard/*.js",
"types": "./dist/dashboard/*.d.ts"
},
"./mappings": {
"import": "./dist/mappings/index.js",
"types": "./dist/mappings/index.d.ts"
},
"./mappings/*": {
"import": "./dist/mappings/*.js",
"types": "./dist/mappings/*.d.ts"
},
"./orders": {
"import": "./dist/orders/index.js",
"types": "./dist/orders/index.d.ts"
},
"./orders/*": {
"import": "./dist/orders/*.js",
"types": "./dist/orders/*.d.ts"
},
"./payments": {
"import": "./dist/payments/index.js",
"types": "./dist/payments/index.d.ts"
},
"./payments/*": {
"import": "./dist/payments/*.js",
"types": "./dist/payments/*.d.ts"
},
"./sim": {
"import": "./dist/sim/index.js",
"types": "./dist/sim/index.d.ts"
},
"./sim/*": {
"import": "./dist/sim/*.js",
"types": "./dist/sim/*.d.ts"
},
"./sim/providers/freebit": {
"import": "./dist/sim/providers/freebit/index.js",
"types": "./dist/sim/providers/freebit/index.d.ts"
},
"./subscriptions": {
"import": "./dist/subscriptions/index.js",
"types": "./dist/subscriptions/index.d.ts"
},
"./subscriptions/*": {
"import": "./dist/subscriptions/*.js",
"types": "./dist/subscriptions/*.d.ts"
},
"./support": {
"import": "./dist/support/index.js",
"types": "./dist/support/index.d.ts"
},
"./support/*": {
"import": "./dist/support/*.js",
"types": "./dist/support/*.d.ts"
},
"./toolkit": {
"import": "./dist/toolkit/index.js",
"types": "./dist/toolkit/index.d.ts"
},
"./toolkit/*": {
"import": "./dist/toolkit/*.js",
"types": "./dist/toolkit/*.d.ts"
}
}, },
"scripts": { "scripts": {
"prebuild": "pnpm run clean", "prebuild": "pnpm run clean",

View File

@ -4,7 +4,10 @@
"composite": true, "composite": true,
"outDir": "./dist", "outDir": "./dist",
"rootDir": ".", "rootDir": ".",
"tsBuildInfoFile": "./dist/.tsbuildinfo" "tsBuildInfoFile": "./dist/.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"verbatimModuleSyntax": false
}, },
"include": [ "include": [
"auth/**/*", "auth/**/*",
@ -13,15 +16,15 @@
"common/**/*", "common/**/*",
"customer/**/*", "customer/**/*",
"dashboard/**/*", "dashboard/**/*",
"providers/**/*",
"mappings/**/*", "mappings/**/*",
"orders/**/*", "orders/**/*",
"payments/**/*", "payments/**/*",
"providers/**/*",
"sim/**/*", "sim/**/*",
"subscriptions/**/*", "subscriptions/**/*",
"support/**/*", "support/**/*",
"toolkit/**/*", "toolkit/**/*",
"index.ts" "index.ts"
], ],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] "exclude": ["node_modules", "dist"]
} }

751
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +1,20 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": { "compilerOptions": {
// Language and Environment (Node.js 22 supports ES2023 features) "target": "ES2024",
"target": "ES2023", "lib": ["ES2024"],
"lib": ["ES2023"],
"module": "commonjs",
"moduleResolution": "node",
// Type Checking - Strict Mode
"strict": true, "strict": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitOverride": true, "noImplicitOverride": true,
"noUncheckedIndexedAccess": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
// Modules
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
// Emit
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"sourceMap": true, "sourceMap": true,
"removeComments": true,
// Interop Constraints
"allowJs": false,
"skipLibCheck": true, "skipLibCheck": true,
// Completeness
"incremental": true "incremental": true
} }
} }

View File

@ -3,10 +3,6 @@
"files": [], "files": [],
"references": [ "references": [
{ "path": "./packages/domain" }, { "path": "./packages/domain" },
{ "path": "./packages/logging" },
{ "path": "./packages/validation" },
{ "path": "./packages/integrations/whmcs" },
{ "path": "./packages/integrations/freebit" },
{ "path": "./apps/bff" }, { "path": "./apps/bff" },
{ "path": "./apps/portal" } { "path": "./apps/portal" }
] ]