name: javascript-frontend-ecosystem description: Implements monorepo architecture, build toolchain selection, module federation, and package health assessment for modern JavaScript frontend ecosystems to enable scalable multi-package development workflows. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: coding triggers: monorepo setup, how do i set up a monorepo, module federation, micro-frontends, build toolchain migration, webpack to vite, turborepo vs nx archetypes:
- tactical
- generation anti_triggers:
- brainstorming
- vague ideation
- code golf
- over-engineering response_profile: verbosity: low directive_strength: high abstraction_level: operational role: implementation scope: implementation output-format: code content-types:
- code
- guidance
- examples
- do-dont related-skills: coding-frontend-testing-patterns, javascript-package-workflows, agent-task-routing
JavaScript Frontend Ecosystem Architect
Implements monorepo architecture decisions, build toolchain selection, module federation for micro-frontends, and package health assessment across modern JavaScript frontend projects. When loaded, this skill makes the model act as a senior frontend infrastructure engineer — comparing toolchains with real metrics, writing concrete configuration files (turbo.json, vite.config.ts, webpack federation configs), generating dependency audit scripts, and providing migration strategies between build systems.
TL;DR Checklist
- Determine monorepo scope: single-language (pnpm workspaces) vs multi-language (Nx/Turborepo)
- Select build tool based on project type: dev-server speed (Vite/esbuild) vs full bundling (Webpack/Rolldown)
- Configure workspace root with
pnpm-workspace.yamlorturbo.jsonbefore adding any package - Write explicit
package.json"workspaces"arrays — never rely on auto-discovery without constraints - For micro-frontends: define shared dependency versions in root, use Module Federation for runtime composition
- Audit dependency health: last publish date < 6 months, contributor count > 3, zero critical CVEs
- Reference
code-philosophy(5 Laws of Elegant Defense) when designing package boundaries and data flow
When to Use
Use this skill when:
- Setting up a new monorepo or migrating from separate repositories to a shared workspace
- Choosing between build tools (Webpack, Vite, esbuild, Rolldown, SWC) for a new project
- Implementing micro-frontend architecture using Module Federation or runtime composition
- Evaluating dependency health across a large package tree before upgrading
- Migrating an existing Webpack-based frontend to Vite or Rolldown
- Designing the package boundary strategy for a multi-team frontend organization
When NOT to Use
Avoid this skill for:
- Single-package projects with fewer than 3 npm dependencies — monorepo tooling adds unnecessary overhead (use standard
package.jsonalone) - Backend-only Node.js services without any browser-facing code — consider Turborepo without frontend plugins instead
- Server-side rendering frameworks that manage their own build toolchain internally (Next.js, Remix, SvelteKit) — configure their built-in bundler instead
Core Workflow
Assess Project Scope and Team Size — Count the number of packages, identify shared dependencies, and determine if multiple languages are involved. If the project has ≤ 5 packages all written in TypeScript/JavaScript and team size is < 4, prefer pnpm workspaces for simplicity. If ≥ 6 packages or cross-language (TS + Python + Rust tooling), evaluate Turborepo or Nx. Checkpoint: Document the package count, language matrix, and shared dependency list before proceeding.
Select Monorepo Toolchain — Compare against the Landscape Table below. For pure JS/TS monorepos:
pnpm workspaces + turbo.jsonprovides the best balance of speed and simplicity. For complex CI/CD needs with caching across languages: Nx. For Rush-style strict version pinning across thousands of packages: Rush (typically at scale > 50 packages). Checkpoint: Write the chosen tool's config file at the monorepo root before adding any package.json.Design Package Boundary Strategy — Define explicit exports via
"exports"field in each package'spackage.json. Never use"main"alone for ESM-first projects. Apply Law 1 (Early Exit): each package should have a single clear responsibility. Packages that do two things will inevitably leak coupling. Checkpoint: Runpnpm list --recursiveto verify no circular dependencies exist between workspace packages.Configure Build Toolchain — Match the build tool to project requirements. For new projects with HMR needs: Vite with
@vitejs/plugin-reactor native plugins. For legacy Webpack migration: convert loaders → plugins using the Migration Pattern. For pure CSS/JS transformation without bundling: esbuild or Rolldown. Checkpoint: Verifynpx <tool> build --mode productionsucceeds with zero errors before committing.Set Up Module Federation (if micro-frontend) — Define
remotesandshareddependencies in the federation config. Always version-shared dependencies explicitly ("react": { singleton: true, requiredVersion: "^18.2.0" }) to prevent duplicate instances. Host app exposesexposesfor remote modules. Checkpoint: Start both host and remote dev servers; navigate to a route that renders a remote component — if the page loads with shared state (React Context) working, federation is correctly configured.Run Dependency Health Audit — Execute the audit script (Pattern 4). Flag any package with: last published > 6 months ago, fewer than 3 contributors in the last year, or critical/high CVEs. Present a remediation plan: pin version, fork and maintain, or replace. Checkpoint: Generate
dependency-audit-report.jsonwith pass/fail per package. No unflagged stale packages may ship to production.
Monorepo Toolchain Landscape (May 2026)
| Feature | pnpm Workspaces + Turborepo | Nx | Rush | Yarn Workspaces |
|---|---|---|---|---|
| Best For | JS/TS monorepos, ≤ 20 packages | Complex multi-language repos, CI/CD integration | Enterprise scale (>50 packages), strict version control | Simple JS monorepos with existing Yarn adoption |
| Dependency Resolution | Hoisted + content-addressed store (disk efficient) | Graph-based hoisting with isolation support | Strict peer dep enforcement, no implicit hoisting | Flat hoisting model |
| Task Runner / Caching | Turborepo: remote caching via Vercel/Railway, pipeline-based | Built-in task graph + distributed caching via Nx Cloud | Rush build pipeline, local file cache only | None — requires external tool (e.g., Lerna) |
| Build Integration | Framework-agnostic; works with Vite, Webpack, esbuild, SWC | Integrated generators for React, Angular, Next.js, Vue | Works with any bundler; minimal opinionation | Same as pnpm workspaces |
| CI/CD Support | Remote build cache (cloud or self-hosted via Turbo SDK) | Nx Cloud: distributed task execution, test splitting | GitHub Actions plugin; no hosted caching option | None built in |
| Learning Curve | Low — pnpm-workspace.yaml + simple turbo.json |
Medium-high — generators, executors, project graph config | High — rush.json, version policy files, strict constraints |
Low — familiar if you know Yarn |
| Monorepo Repo Size Limit | Practical limit ~20 packages for optimal UX | Scales to 100+ with Nx Cloud caching | Tested at 3000+ packages (Microsoft internal) | Practical limit ~15 packages |
| License | MIT (Turborepo) / ISC (pnpm) | MIT | Apache-2.0 | BSD-2-Clause |
Build Toolchain Comparison (May 2026)
| Feature | Webpack 5 | Vite 6.x | esbuild / Rolldown | SWC (with tsup/bun build) |
|---|---|---|---|---|
| Primary Use Case | Legacy apps, complex plugin ecosystems, code splitting with dynamic imports | New projects, fast dev HMR, modern framework integration | Pure transformation (no bundling), library builds, zero-config setups | TypeScript-to-JS transform + bundling for libraries |
| Dev Server Start Time | 3–15 seconds (full bundle) | 50–300ms (ECMAScript modules on demand) | N/A (no dev server) | N/A (build-only) |
| Production Build Speed | 30–120 seconds (depending on size) | 5–15 seconds (Rollup under the hood) | 1–5 seconds (transformation only) | 2–10 seconds (parallelized SWC workers) |
| ESM Support | ✅ with output.module: true + imports field |
✅ Native — dev uses native ESM, prod bundles to ESM/CJS | ❌ Transformation only, no module resolution | ❌ Transformation only, use tsup for bundling |
| Tree Shaking | ✅ Side-effect-free detection (sideEffects: false) |
✅ Rollup-based, reliable | ❌ Not applicable (no bundling) | ✅ When bundled via esbuild/terser downstream |
| Plugin Ecosystem | 4000+ plugins on npm | Growing (Vite ecosystem), Rollup plugin compatibility layer | Rolldown: rapid growth in 2025–2026, Rust-based | Limited — mostly transform hooks |
| CSS Handling | css-loader + style-loader / mini-css-extract-plugin | Native CSS import, separate chunks with @vitejs/plugin-basic-ssl |
Not applicable (use postcss CLI) | Use postcss as preprocessor |
| Code Splitting | ✅ Dynamic import() with chunk naming via optimization.splitChunks |
✅ Rollup dynamic imports + manual chunk groups | ❌ No splitting — single file per entry | ✅ Via esbuild's splitting: true in tsup |
| TypeScript Support | Requires ts-loader or @swc/core (slower) |
✅ Native, uses esbuild for transform | N/A (TS transformed by Rolldown/swc internally) | ✅ Native SWC compiler — fastest TS transform |
| Migration Effort | — | Medium: convert loaders to plugins, update import syntax | Low: drop-in replacement for tsc --outDir |
Medium: restructure package.json "build" scripts |
| Recommended For | Maintaining existing Webpack projects | Greenfield projects, framework migrations | Library publishing, CSS/JS minification pipelines | TypeScript library packaging with zero-config builds |
Implementation Patterns
Pattern 1: Monorepo Workspace Configuration — pnpm Workspaces + Turborepo
Set up a production-grade monorepo root with explicit workspace definitions, pipeline caching, and shared ESLint config.
# packages/monorepo-root/pnpm-workspace.yaml
# Defines which directories contain workspace packages.
# Explicit arrays prevent accidental inclusion of node_modules or dist folders.
packages:
- 'apps/*' # Application packages (web-frontend, admin-dashboard)
- 'packages/*' # Shared library packages (ui-components, data-fetcher, auth-utils)
- '!packages/*/dist' # Exclude build output directories
// packages/monorepo-root/turbo.json
{
"$schema": "https://turbo.build/schema.json",
// Pipeline defines which tasks run and their dependencies.
// Each task can declare input files for caching granularity.
"tasks": {
"build": {
// Input sources tracked for cache invalidation
"inputs": ["src/**/*.ts", "src/**/*.tsx", "*.json"],
// Output artifacts to cache remotely
"outputs": ["dist/**", ".next/**", "build/**"],
// Upstream dependencies: build depends on the package's own build task first
"dependsOn": ["^build"],
// Cache is enabled and uses a remote cache (Vercel/Railway/self-hosted)
"cache": true
},
"dev": {
// Dev never hits remote cache — always run locally with streaming output
"cache": false,
"persistent": true,
"dependsOn": []
},
"lint": {
"inputs": ["src/**/*.ts", "src/**/*.tsx", "*.json", ".eslintrc*"],
"outputs": [],
"cache": true,
"dependsOn": []
},
"test": {
// Integration tests depend on build; unit tests do not
"inputs": ["src/**/*.ts", "src/**/*.tsx", "tests/**"],
"outputs": [],
"cache": true,
"dependsOn": ["^build"]
},
"typecheck": {
"inputs": ["src/**/*.ts", "src/**/*.tsx", "tsconfig*.json"],
"outputs": [],
"cache": false,
"dependsOn": []
}
}
}
// packages/monorepo-root/package.json — Root workspace manifest
{
"name": "js-frontend-monorepo",
"private": true,
"scripts": {
// Run build across all packages in dependency order
"build": "turbo run build",
// Start dev servers for all apps concurrently
"dev": "turbo run dev --parallel",
// Lint everything
"lint": "turbo run lint",
// Test everything with typecheck first
"test": "turbo run test",
// Type-check without emitting files (fast)
"typecheck": "turbo run typecheck"
},
"devDependencies": {
"typescript": "^5.6.0",
"eslint": "^9.18.0",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0"
},
"engines": {
"node": ">=20.0.0",
"pnpm": ">=9.0.0"
}
}
❌ BAD — Using pnpm workspaces with implicit auto-discovery (fragile):
# pnpm-workspace.yaml — BAD: no explicit packages list, no exclusions
# Any subdirectory could be picked up as a package, including CI configs
# and temporary build artifacts. This leads to spurious errors during
# `pnpm install`.
packages:
- '*/' # Too broad — catches node_modules, .git, temp dirs
✅ GOOD — Explicit packages list with exclusion patterns: See the full example above. Always be explicit about what is and isn't a workspace package.
Pattern 2: Module Federation Setup for Micro-Frontends (Webpack 5)
Configure runtime composition between a host application and remote micro-frontend using Webpack 5's Module Federation plugin.
// apps/host-app/webpack.config.js — Host Application
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
entry: './src/index.tsx',
output: {
filename: '[name].[contenthash:8].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
module: {
rules: [
{
test: /\.[jt]sx?$/,
use: 'swc-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new ModuleFederationPlugin({
name: 'hostApp',
filename: 'remoteEntry.js',
// Remote exposes can be consumed by other hosts
remotes: {
// Load the dashboard micro-frontend from its dev server URL
// In production, this would be a CDN path instead
dashboard: 'dashboardApp@http://localhost:3001/remoteEntry.js',
},
// Shared dependencies — critical for preventing duplicate React instances
shared: {
react: {
singleton: true, // Only one React instance allowed
requiredVersion: '^18.2.0', // Pin compatible version range
eager: false, // Lazy-load until needed (performance)
},
'react-dom': {
singleton: true,
requiredVersion: '^18.2.0',
},
'@internal/ui-components': {
singleton: false, // Each app can have its own copy
requiredVersion: '^2.0.0',
},
'react-router-dom': {
singleton: true,
requiredVersion: '^6.22.0',
eager: true, // Must be loaded before any route matching
},
},
}),
],
devServer: {
port: 3000,
historyApiFallback: true,
},
};
// apps/dashboard-app/webpack.config.js — Remote Micro-Frontend
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
module.exports = {
entry: './src/index.tsx',
output: {
filename: '[name].[contenthash:8].js',
path: path.resolve(__dirname, 'dist'),
clean: true,
publicPath: 'http://localhost:3001/', // Must match dev server URL
},
module: {
rules: [
{ test: /\.[jt]sx?$/, use: 'swc-loader', exclude: /node_modules/ },
{ test: /\.css$/, use: ['style-loader', 'css-loader'] },
],
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new ModuleFederationPlugin({
name: 'dashboardApp',
filename: 'remoteEntry.js',
// Expose these modules for host apps to consume
exposes: {
'./DashboardPage': './src/pages/DashboardPage.tsx',
'./AnalyticsWidget': './src/widgets/AnalyticsWidget.tsx',
},
shared: {
react: { singleton: true, requiredVersion: '^18.2.0' },
'react-dom': { singleton: true, requiredVersion: '^18.2.0' },
'react-router-dom': { singleton: true, requiredVersion: '^6.22.0' },
},
}),
],
devServer: {
port: 3001,
headers: {
// Module Federation requires CORS for cross-origin remoteEntry.js fetches
'Access-Control-Allow-Origin': '*',
},
},
};
// apps/host-app/src/App.tsx — Consuming a remote module dynamically
import React, { lazy, Suspense } from 'react';
// Dynamic import of the remote module — loads only when the route is visited
const DashboardPage = lazy(() => import('dashboardApp/DashboardPage'));
function App() {
return (
<Suspense fallback={
<div className="skeleton-loader" style={{ padding: '2rem' }}>
Loading dashboard...
</div>
}>
<DashboardPage />
</Suspense>
);
}
export default App;
❌ BAD — Missing shared dependency version pinning (causes duplicate React):
// BAD: Without requiredVersion, a remote using React 19 and host using React 18
// will cause "Invalid hook call" errors at runtime because two React instances exist.
shared: {
react: { singleton: true }, // No version constraint!
'react-dom': { singleton: true }, // Same problem
}
✅ GOOD — Explicit shared dependency contracts:
See the full example above. Always pin requiredVersion for all shared dependencies and use singleton: true only for framework core packages that must have a single instance.
Pattern 3: Build Toolchain Migration — Webpack → Vite (with Real Conversion)
Convert an existing Webpack configuration to Vite, handling loader-to-plugin mappings, alias resolution, and environment variable differences.
// OLD: apps/webapp/webpack.config.js (legacy configuration)
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: './src/index.tsx',
output: {
filename: '[name].[contenthash:8].js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
// TypeScript compilation via SWC (old Webpack config used babel-loader)
{ test: /\.[jt]sx?$/, use: ['swc-loader'], exclude: /node_modules/ },
// CSS extraction to separate files
{ test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] },
// Image assets with inline threshold
{ test: /\.(png|svg|jpg|jpeg|gif)$/i, type: 'asset', parser: { dataUrlCondition: { maxSize: 8192 } } },
// Font files
{ test: /\.(woff|woff2|eot|ttf|otf)$/i, type: 'asset/resource', generator: { filename: 'fonts/[name][ext]' } },
],
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
alias: {
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@hooks': path.resolve(__dirname, 'src/hooks'),
},
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html', minify: { removeComments: true } }),
new MiniCssExtractPlugin({ filename: '[name].[contenthash:8].css' }),
new CopyPlugin({ patterns: [{ from: 'public/', to: '.' }] }),
],
};
// NEW: apps/webapp/vite.config.ts (converted Vite configuration)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import path from 'path';
export default defineConfig({
plugins: [
// SWC-based React plugin — faster than the babel-based @vitejs/plugin-react
react({
tsDecorators: true,
}),
],
resolve: {
alias: {
// Vite aliases use string prefixes — no need for path.resolve()
'@components/': new URL('./src/components/', import.meta.url).pathname,
'@utils/': new URL('./src/utils/', import.meta.url).pathname,
'@hooks/': new URL('./src/hooks/', import.meta.url).pathname,
},
},
build: {
// Output configuration matches the Webpack pattern
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
// Manual chunk splitting — same strategy as Webpack's splitChunks
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
charts: ['recharts', 'd3'],
},
},
},
},
css: {
// CSS handling is native in Vite — no loader configuration needed
// PostCSS can be configured via postcss.config.js
devSourcemap: true,
},
server: {
port: 3000,
hmr: {
overlay: true,
},
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
},
},
});
// NEW: apps/webapp/postcss.config.mjs (new file — replaces css-loader config)
export default {
plugins: {
autoprefixer: {},
'postcss-import': {},
},
};
❌ BAD — Using Webpack-style require() for environment variables in Vite:
// BAD: process.env does not work reliably in client-side Vite code.
// Vite exposes env via import.meta.env, not process.env.
const apiBaseUrl = process.env.REACT_APP_API_URL;
✅ GOOD — Vite-native environment variable access with type-safe prefix:
// GOOD: Vite only exposes env variables prefixed with VITE_ into the client bundle.
// Using import.meta.env provides type safety and guarantees the value exists at compile time.
const apiBaseUrl = import.meta.env.VITE_API_URL;
// For build-time-only env vars (server config), access them in a .ts file that
// never gets bundled to the browser:
// const secretKey = process.env.SECRET_KEY; // OK — server-side code only
Pattern 4: Package Health Assessment and Dependency Audit Script
Generate a comprehensive health report for all dependencies across a monorepo, checking registry status, publication recency, contributor activity, and known vulnerabilities.
// scripts/dependency-audit.ts — Monorepo dependency health auditor
import { execSync } from 'node:child_process';
import { readFileSync, writeFileSync } from 'node:fs';
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, '..');
// Configuration thresholds — tune these for your organization's risk tolerance
const THRESHOLDS = {
maxAgeMonths: 6, // Package not published in N months → flagged
minContributors: 3, // Fewer than M unique contributors in last year → flagged
maxCriticalVulns: 0, // Zero critical CVEs allowed
maxHighVulns: 2, // More than 2 high-severity CVEs → flagged
minWeeklyDownloads: 100, // Fewer than K downloads/week → flagged as low-usage dependency
} as const;
interface DependencyInfo {
name: string;
version: string;
type: 'prod' | 'dev';
lastPublished: string | null;
ageMonths: number | null;
contributors: number | null;
criticalVulns: number;
highVulns: number;
weeklyDownloads: number | null;
healthStatus: 'healthy' | 'warning' | 'critical';
reasons: string[];
}
interface AuditReport {
timestamp: string;
totalDependencies: number;
healthy: number;
warnings: number;
critical: number;
dependencies: DependencyInfo[];
}
/**
* Fetch package metadata from the npm registry.
* Uses the public JSON API — no authentication required for read access.
*/
async function fetchPackageMetadata(
name: string,
version: string | null = null,
): Promise<{
lastPublished: string;
contributors: number;
weeklyDownloads: number;
}> {
try {
const url = version
? `https://registry.npmjs.org/${name}/${version}`
: `https://registry.npmjs.org/${name}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Registry returned ${response.status} for ${name}`);
}
const data = await response.json();
const distTags = data['dist-tags'] as Record<string, string>;
const actualVersion = version || distTags.latest || Object.keys(data.versions)[0];
const versionData = data.versions[actualVersion];
return {
lastPublished: data.time?.[actualVersion] ?? null,
contributors: versionData?.contributors
? Array.isArray(versionData.contributors)
? versionData.contributors.length
: 1
: null,
weeklyDownloads: data.downloads?.lastWeek ?? null,
};
} catch (error) {
// If registry fetch fails, return partial metadata — don't abort the whole audit
console.warn(`[WARN] Could not fetch metadata for ${name}: ${(error as Error).message}`);
return { lastPublished: null, contributors: null, weeklyDownloads: null };
}
}
/**
* Query npm audit for vulnerability counts.
* Returns a summary object with critical and high CVE counts.
*/
function runNpmAudit(workingDir: string): { critical: number; high: number } {
try {
const output = execSync(
`npm audit --json --prefix "${workingDir}"`,
{ encoding: 'utf-8', timeout: 30_000 },
);
const auditData = JSON.parse(output);
const vulnerabilities = auditData.vulnerabilities as Record<string, { severity: string }>;
let critical = 0;
let high = 0;
for (const [name, info] of Object.entries(vulnerabilities)) {
if (info.severity === 'critical') critical++;
if (info.severity === 'high') high++;
}
return { critical, high };
} catch {
// Audit might fail in CI with --ignore-scripts or locked environments
// Return zeros — the developer should run `npm audit` locally for accurate results
return { critical: 0, high: 0 };
}
}
/**
* Calculate the age of a package in months from its last published date.
*/
function calculateAgeMonths(lastPublished: string | null): number | null {
if (!lastPublished) return null;
const pubDate = new Date(lastPublished);
const now = new Date();
if (isNaN(pubDate.getTime())) return null;
const diffMs = now.getTime() - pubDate.getTime();
const diffMonths = Math.floor(diffMs / (1000 * 60 * 60 * 24 * 30.44));
return diffMonths;
}
/**
* Determine health status and collect all reasons for flags.
*/
function assessHealth(
ageMonths: number | null,
contributors: number | null,
criticalVulns: number,
highVulns: number,
weeklyDownloads: number | null,
): { status: 'healthy' | 'warning' | 'critical'; reasons: string[] } {
const reasons: string[] = [];
if (ageMonths !== null && ageMonths > THRESHOLDS.maxAgeMonths) {
reasons.push(`Not published in ${ageMonths} months (threshold: ${THRESHOLDS.maxAgeMonths})`);
}
if (contributors !== null && contributors < THRESHOLDS.minContributors) {
reasons.push(`Only ${contributors} contributor(s) (minimum: ${THRESHOLDS.minContributors})`);
}
if (criticalVulns > THRESHOLDS.maxCriticalVulns) {
reasons.push(`${criticalVulns} critical CVE(s) found`);
}
if (highVulns > THRESHOLDS.maxHighVulns) {
reasons.push(`${highVulns} high-severity CVE(s) found`);
}
if (weeklyDownloads !== null && weeklyDownloads < THRESHOLDS.minWeeklyDownloads) {
reasons.push(`Low adoption: ${weeklyDownloads} weekly downloads`);
}
const status: 'healthy' | 'warning' | 'critical' =
criticalVulns > 0 || ageMonths === null ? 'critical' :
reasons.length > 0 ? 'warning' : 'healthy';
return { status, reasons };
}
/**
* Main audit function — scans all workspaces and produces a consolidated report.
*/
async function runMonorepoAudit(): Promise<AuditReport> {
const report: AuditReport = {
timestamp: new Date().toISOString(),
totalDependencies: 0,
healthy: 0,
warnings: 0,
critical: 0,
dependencies: [],
};
// Step 1: Discover all workspace package.json files using pnpm
let workspacePackages: string[] = [];
try {
const output = execSync('pnpm ls --depth=-1 --json', {
cwd: ROOT,
encoding: 'utf-8',
timeout: 60_000,
});
// Parse pnpm's flat list to get unique workspace package paths
const pkgPaths = new Set<string>();
for (const line of output.trim().split('\n')) {
if (line.startsWith('[') && line.endsWith(']')) continue;
// Extract the root directory of each workspace package
const match = line.match(/^(@[^\/]+\/)?([^@\/]+)/);
if (match) {
pkgPaths.add(match[2]);
}
}
// Also directly scan the workspace structure
for (const subdir of ['apps', 'packages']) {
const dirPath = resolve(ROOT, subdir);
try {
const dirs = execSync(`find "${dirPath}" -mindepth 1 -maxdepth 1 -type d`, {
encoding: 'utf-8',
timeout: 5_000,
}).trim().split('\n').filter(Boolean);
for (const dir of dirs) {
pkgPaths.add(dir);
}
} catch { /* directory may not exist */ }
}
workspacePackages = Array.from(pkgPaths);
} catch {
console.error('[ERROR] Could not discover workspace packages. Ensure you are in a pnpm monorepo root.');
process.exit(1);
}
// Step 2: Audit each workspace package individually
for (const pkgPath of workspacePackages) {
const pkgJsonPath = resolve(pkgPath, 'package.json');
try {
const pkgContent = readFileSync(pkgJsonPath, 'utf-8');
const pkgJson = JSON.parse(pkgContent);
const pkgName = pkgJson.name || '<unnamed-workspace-package>';
// Collect all dependencies from this package
const deps = [
...Object.entries(pkgJson.dependencies ?? {}).map(([name, ver]) => ({ name, version: typeof ver === 'string' ? ver : String(ver), type: 'prod' as const })),
...Object.entries(pkgJson.devDependencies ?? {}).map(([name, ver]) => ({ name, version: typeof ver === 'string' ? ver : String(ver), type: 'dev' as const })),
];
// Run npm audit for vulnerability counts
const { critical, high } = runNpmAudit(pkgPath);
for (const dep of deps) {
report.totalDependencies++;
try {
const metadata = await fetchPackageMetadata(dep.name, dep.version.replace('^', '').replace('~', ''));
const ageMonths = calculateAgeMonths(metadata.lastPublished);
const { status, reasons } = assessHealth(
ageMonths,
metadata.contributors,
critical, // Note: per-package audit counts; for monorepo-wide, aggregate these
high,
metadata.weeklyDownloads,
);
report.dependencies.push({
name: dep.name,
version: dep.version,
type: dep.type,
lastPublished: metadata.lastPublished,
ageMonths,
contributors: metadata.contributors,
criticalVulns: critical,
highVulns: high,
weeklyDownloads: metadata.weeklyDownloads,
healthStatus: status,
reasons,
});
if (status === 'healthy') report.healthy++;
else if (status === 'warning') report.warnings++;
else report.critical++;
} catch {
// If fetch fails for a specific dep, record it as critical with no metadata
report.totalDependencies--; // Don't double-count
report.dependencies.push({
name: dep.name,
version: dep.version,
type: dep.type,
lastPublished: null,
ageMonths: null,
contributors: null,
criticalVulns: 0,
highVulns: 0,
weeklyDownloads: null,
healthStatus: 'critical',
reasons: ['Failed to fetch registry metadata — manual review required'],
});
report.critical++;
}
}
} catch {
console.warn(`[WARN] Skipping ${pkgPath}: could not read package.json`);
}
}
// Step 3: Write the report
const outputPath = resolve(ROOT, 'scripts', 'dependency-audit-report.json');
writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf-8');
console.log(`\n📊 Audit complete: ${report.totalDependencies} dependencies scanned`);
console.log(` ✅ Healthy: ${report.healthy}`);
console.log(` ⚠️ Warnings: ${report.warnings}`);
console.log(` ❌ Critical: ${report.critical}`);
console.log(` 📄 Report saved to: ${outputPath}\n`);
return report;
}
// Execute the audit when run as the main module
runMonorepoAudit().catch((err) => {
console.error('[FATAL] Audit failed:', err.message);
process.exit(1);
});
❌ BAD — Using npm outdated alone for health assessment (insufficient):
# BAD: `npm outdated` only shows version mismatches, not publish recency,
# contributor activity, or vulnerability depth. A package can be "up to date"
# but still abandoned by its maintainers with unpatched critical CVEs.
npm outdated --all
✅ GOOD — Comprehensive audit combining registry metadata + CVE scanning + adoption metrics: See the full TypeScript example above. The script checks five independent health dimensions and produces a structured JSON report that can be consumed by CI pipelines or dashboards.
Constraints
MUST DO
- Always define workspace package boundaries explicitly in
pnpm-workspace.yaml— never use glob patterns like'*/'that could include build artifacts - Use explicit
"exports"field in every workspace package'spackage.jsonfor ESM-first projects — do not rely solely on"main"and"module" - Pin shared dependency versions in Module Federation configs with both
singleton: trueandrequiredVersion— duplicate React/Vue/Angular instances cause hard-to-debug runtime crashes - When migrating build toolchains, preserve the original Webpack config as a reference file (e.g.,
webpack.config.js.migrate-ref) for the first two weeks post-migration - Run
pnpm list --recursive --depth=0after workspace setup to verify no circular dependencies before committing configuration - Audit all third-party dependencies against these five health dimensions: last published date, contributor count, critical CVEs, high CVEs, and weekly download count
- Reference
code-philosophy(5 Laws of Elegant Defense): design package boundaries so data flows naturally from public APIs inward, use early exit for invalid dependency states, and fail fast with descriptive errors when audit thresholds are breached
MUST NOT DO
- Never share
node_modulesacross workspace packages without content-addressed storage (pnpm) — symlink hoisting causes broken resolution in nested workspaces - Do not use
"main": "dist/index.js"as the sole export path for ESM packages — always include"exports"with both ESM and CJS entry points - Do not skip CORS headers on Module Federation remote dev servers — without
'Access-Control-Allow-Origin': '*', the host app cannot fetchremoteEntry.jsacross ports - Never set
singleton: trueon every shared dependency in Module Federation — only core framework packages (react, react-dom, react-router) should be singletons; UI component libraries should allow duplicates per micro-frontend - Do not rely on
npm outdatedalone for dependency health — it measures version age, not project vitality or security posture - Never commit generated
dist/or.next/directories to version control in any workspace package
Output Template
When applying this skill, produce:
- Architecture Recommendation — Selected monorepo toolchain with justification based on the Landscape Table metrics (package count, team size, language diversity)
- Configuration Files — Complete
pnpm-workspace.yaml,turbo.jsonornx.json, and rootpackage.jsonready to commit - Build Tool Recommendation — Selected bundler with justification from the Build Toolchain Comparison Table, including migration steps if applicable
- Module Federation Config — Host and remote Webpack configs with shared dependency contracts (if micro-frontend architecture is required)
- Dependency Audit Report — JSON structure summary showing pass/fail counts per health dimension, with flagged packages listed explicitly
- Migration Checklist — Step-by-step conversion plan for build toolchain migrations, including what config lines map to what
Live References
- pnpm Workspaces Documentation — https://pnpm.io/workspaces (official workspace configuration guide)
- Turborepo Documentation — https://turbo.build/repo/docs (task pipelines, remote caching, and monorepo patterns)
- Module Federation Specification — https://module-federation.io/guide/self-hosted.html (runtime module composition for micro-frontends)
- Vite Documentation — https://vitejs.dev/guide/ (configuration reference, plugin API, build optimization)
- Webpack 5 Module Federation Guide — https://webpack.js.org/plugins/module-federation-plugin/ (official migration and federation setup)
- npm Audit CLI Reference — https://docs.npmjs.com/cli/audit (dependency vulnerability scanning commands and JSON output format)
- Rolldown (Rust-based Webpack successor) — https://rolldown.rs/ (next-generation bundler with ESM-native architecture)
Related Skills
| Skill | Purpose |
|---|---|
coding-frontend-testing-patterns |
Write testing strategies (unit, integration, e2e) for monorepo frontend apps using Vitest and Playwright |
coding-package-publishing-workflows |
Configure semantic-release, changelog generation, and npm registry publishing across workspace packages |
agent-task-routing |
Route frontend infrastructure tasks to the appropriate specialized skills based on domain classification |