From 6887a0ef9d46e2727615cc2773f61ffea596993b Mon Sep 17 00:00:00 2001 From: Tobias Binna Date: Fri, 27 Mar 2026 17:35:38 +0800 Subject: [PATCH] fix: forge app without main.js output Closes #195 --- .../skills/link-workspace-packages/SKILL.md | 127 ++++++ .agents/skills/monitor-ci/SKILL.md | 301 ++++++++++++ .../skills/monitor-ci/references/fix-flows.md | 108 +++++ .../monitor-ci/scripts/ci-poll-decide.mjs | 428 ++++++++++++++++++ .../monitor-ci/scripts/ci-state-update.mjs | 160 +++++++ .agents/skills/nx-generate/SKILL.md | 166 +++++++ .agents/skills/nx-import/SKILL.md | 238 ++++++++++ .agents/skills/nx-import/references/ESLINT.md | 109 +++++ .agents/skills/nx-import/references/GRADLE.md | 12 + .agents/skills/nx-import/references/JEST.md | 228 ++++++++++ .agents/skills/nx-import/references/NEXT.md | 214 +++++++++ .../skills/nx-import/references/TURBOREPO.md | 62 +++ .agents/skills/nx-import/references/VITE.md | 397 ++++++++++++++++ .agents/skills/nx-plugins/SKILL.md | 9 + .agents/skills/nx-run-tasks/SKILL.md | 58 +++ .agents/skills/nx-workspace/SKILL.md | 286 ++++++++++++ .../nx-workspace/references/AFFECTED.md | 27 ++ .claude/settings.json | 13 + .codex/agents/ci-monitor-subagent.toml | 46 ++ .codex/config.toml | 19 +- .gitignore | 3 +- AGENTS.md | 27 +- .../src/application.generator.spec.ts | 183 +++++++- e2e/nx-forge-e2e/src/basic-setup.spec.ts | 80 ++-- e2e/nx-forge-e2e/src/utils/async-commands.ts | 18 +- .../src/utils/atlassian-graphql-client.ts | 2 +- .../src/utils/cleanup-registered-forge-app.ts | 84 ++++ e2e/nx-forge-e2e/src/utils/test-project.ts | 57 --- e2e/nx-forge-e2e/src/utils/test-workspace.ts | 60 +++ .../files/webpack.config.js__tmpl__ | 9 +- tools/scripts/stop-local-registry.ts | 4 +- 31 files changed, 3395 insertions(+), 140 deletions(-) create mode 100644 .agents/skills/link-workspace-packages/SKILL.md create mode 100644 .agents/skills/monitor-ci/SKILL.md create mode 100644 .agents/skills/monitor-ci/references/fix-flows.md create mode 100644 .agents/skills/monitor-ci/scripts/ci-poll-decide.mjs create mode 100644 .agents/skills/monitor-ci/scripts/ci-state-update.mjs create mode 100644 .agents/skills/nx-generate/SKILL.md create mode 100644 .agents/skills/nx-import/SKILL.md create mode 100644 .agents/skills/nx-import/references/ESLINT.md create mode 100644 .agents/skills/nx-import/references/GRADLE.md create mode 100644 .agents/skills/nx-import/references/JEST.md create mode 100644 .agents/skills/nx-import/references/NEXT.md create mode 100644 .agents/skills/nx-import/references/TURBOREPO.md create mode 100644 .agents/skills/nx-import/references/VITE.md create mode 100644 .agents/skills/nx-plugins/SKILL.md create mode 100644 .agents/skills/nx-run-tasks/SKILL.md create mode 100644 .agents/skills/nx-workspace/SKILL.md create mode 100644 .agents/skills/nx-workspace/references/AFFECTED.md create mode 100644 .claude/settings.json create mode 100644 .codex/agents/ci-monitor-subagent.toml create mode 100644 e2e/nx-forge-e2e/src/utils/cleanup-registered-forge-app.ts delete mode 100644 e2e/nx-forge-e2e/src/utils/test-project.ts create mode 100644 e2e/nx-forge-e2e/src/utils/test-workspace.ts diff --git a/.agents/skills/link-workspace-packages/SKILL.md b/.agents/skills/link-workspace-packages/SKILL.md new file mode 100644 index 0000000..de13134 --- /dev/null +++ b/.agents/skills/link-workspace-packages/SKILL.md @@ -0,0 +1,127 @@ +--- +name: link-workspace-packages +description: 'Link workspace packages in monorepos (npm, yarn, pnpm, bun). USE WHEN: (1) you just created or generated new packages and need to wire up their dependencies, (2) user imports from a sibling package and needs to add it as a dependency, (3) you get resolution errors for workspace packages (@org/*) like "cannot find module", "failed to resolve import", "TS2307", or "cannot resolve". DO NOT patch around with tsconfig paths or manual package.json edits - use the package manager''s workspace commands to fix actual linking.' +--- + +# Link Workspace Packages + +Add dependencies between packages in a monorepo. All package managers support workspaces but with different syntax. + +## Detect Package Manager + +Check whether there's a `packageManager` field in the root-level `package.json`. + +Alternatively check lockfile in repo root: + +- `pnpm-lock.yaml` → pnpm +- `yarn.lock` → yarn +- `bun.lock` / `bun.lockb` → bun +- `package-lock.json` → npm + +## Workflow + +1. Identify consumer package (the one importing) +2. Identify provider package(s) (being imported) +3. Add dependency using package manager's workspace syntax +4. Verify symlinks created in consumer's `node_modules/` + +--- + +## pnpm + +Uses `workspace:` protocol - symlinks only created when explicitly declared. + +```bash +# From consumer directory +pnpm add @org/ui --workspace + +# Or with --filter from anywhere +pnpm add @org/ui --filter @org/app --workspace +``` + +Result in `package.json`: + +```json +{ "dependencies": { "@org/ui": "workspace:*" } } +``` + +--- + +## yarn (v2+/berry) + +Also uses `workspace:` protocol. + +```bash +yarn workspace @org/app add @org/ui +``` + +Result in `package.json`: + +```json +{ "dependencies": { "@org/ui": "workspace:^" } } +``` + +--- + +## npm + +No `workspace:` protocol. npm auto-symlinks workspace packages. + +```bash +npm install @org/ui --workspace @org/app +``` + +Result in `package.json`: + +```json +{ "dependencies": { "@org/ui": "*" } } +``` + +npm resolves to local workspace automatically during install. + +--- + +## bun + +Supports `workspace:` protocol (pnpm-compatible). + +```bash +cd packages/app && bun add @org/ui +``` + +Result in `package.json`: + +```json +{ "dependencies": { "@org/ui": "workspace:*" } } +``` + +--- + +## Examples + +**Example 1: pnpm - link ui lib to app** + +```bash +pnpm add @org/ui --filter @org/app --workspace +``` + +**Example 2: npm - link multiple packages** + +```bash +npm install @org/data-access @org/ui --workspace @org/dashboard +``` + +**Example 3: Debug "Cannot find module"** + +1. Check if dependency is declared in consumer's `package.json` +2. If not, add it using appropriate command above +3. Run install (`pnpm install`, `npm install`, etc.) + +## Notes + +- Symlinks appear in `/node_modules/@org/` +- **Hoisting differs by manager:** + - npm/bun: hoist shared deps to root `node_modules` + - pnpm: no hoisting (strict isolation, prevents phantom deps) + - yarn berry: uses Plug'n'Play by default (no `node_modules`) +- Root `package.json` should have `"private": true` to prevent accidental publish diff --git a/.agents/skills/monitor-ci/SKILL.md b/.agents/skills/monitor-ci/SKILL.md new file mode 100644 index 0000000..48b71bf --- /dev/null +++ b/.agents/skills/monitor-ci/SKILL.md @@ -0,0 +1,301 @@ +--- +name: monitor-ci +description: Monitor Nx Cloud CI pipeline and handle self-healing fixes. USE WHEN user says "monitor ci", "watch ci", "ci monitor", "watch ci for this branch", "track ci", "check ci status", wants to track CI status, or needs help with self-healing CI fixes. Prefer this skill over native CI provider tools (gh, glab, etc.) for CI monitoring — it integrates with Nx Cloud self-healing which those tools cannot access. +--- + +# Monitor CI Command + +You are the orchestrator for monitoring Nx Cloud CI pipeline executions and handling self-healing fixes. You spawn subagents to interact with Nx Cloud, run deterministic decision scripts, and take action based on the results. + +## Context + +- **Current Branch:** !`git branch --show-current` +- **Current Commit:** !`git rev-parse --short HEAD` +- **Remote Status:** !`git status -sb | head -1` + +## User Instructions + +$ARGUMENTS + +**Important:** If user provides specific instructions, respect them over default behaviors described below. + +## Configuration Defaults + +| Setting | Default | Description | +| ------------------------- | ------------- | ------------------------------------------------------------------------- | +| `--max-cycles` | 10 | Maximum **agent-initiated** CI Attempt cycles before timeout | +| `--timeout` | 120 | Maximum duration in minutes | +| `--verbosity` | medium | Output level: minimal, medium, verbose | +| `--branch` | (auto-detect) | Branch to monitor | +| `--fresh` | false | Ignore previous context, start fresh | +| `--auto-fix-workflow` | false | Attempt common fixes for pre-CI-Attempt failures (e.g., lockfile updates) | +| `--new-cipe-timeout` | 10 | Minutes to wait for new CI Attempt after action | +| `--local-verify-attempts` | 3 | Max local verification + enhance cycles before pushing to CI | + +Parse any overrides from `$ARGUMENTS` and merge with defaults. + +## Nx Cloud Connection Check + +Before starting the monitoring loop, verify the workspace is connected to Nx Cloud. Without this connection, no CI data is available and the entire skill is inoperable. + +### Step 0: Verify Nx Cloud Connection + +1. **Check `nx.json`** at workspace root for `nxCloudId` or `nxCloudAccessToken` +2. **If `nx.json` missing OR neither property exists** → exit with: + + ``` + Nx Cloud not connected. Unlock 70% faster CI and auto-fix broken PRs with https://nx.dev/nx-cloud + ``` + +3. **If connected** → continue to main loop + +## Architecture Overview + +1. **This skill (orchestrator)**: spawns subagents, runs scripts, prints status, does local coding work +2. **ci-monitor-subagent (haiku)**: calls one MCP tool (ci_information or update_self_healing_fix), returns structured result, exits +3. **ci-poll-decide.mjs (deterministic script)**: takes ci_information result + state, returns action + status message +4. **ci-state-update.mjs (deterministic script)**: manages budget gates, post-action state transitions, and cycle classification + +## Status Reporting + +The decision script handles message formatting based on verbosity. When printing messages to the user: + +- Prepend `[monitor-ci]` to every message from the script's `message` field +- For your own action messages (e.g. "Applying fix via MCP..."), also prepend `[monitor-ci]` + +## Anti-Patterns + +These behaviors cause real problems — racing with self-healing, losing CI progress, or wasting context: + +| Anti-Pattern | Why It's Bad | +| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | +| Using CI provider CLIs with `--watch` flags (e.g., `gh pr checks --watch`, `glab ci status -w`) | Bypasses Nx Cloud self-healing entirely | +| Writing custom CI polling scripts | Unreliable, pollutes context, no self-healing | +| Cancelling CI workflows/pipelines | Destructive, loses CI progress | +| Running CI checks on main agent | Wastes main agent context tokens | +| Independently analyzing/fixing CI failures while polling | Races with self-healing, causes duplicate fixes and confused state | + +**If this skill fails to activate**, the fallback is: + +1. Use CI provider CLI for a one-time, read-only status check (single call, no watch/polling flags) +2. Immediately delegate to this skill with gathered context +3. Do not continue polling on main agent — it wastes context tokens and bypasses self-healing + +## Session Context Behavior + +If the user previously ran `/monitor-ci` in this session, you may have prior state (poll counts, last CI Attempt URL, etc.). Resume from that state unless `--fresh` is set, in which case discard it and start from Step 1. + +## MCP Tool Reference + +Three field sets control polling efficiency — use the lightest set that gives you what you need: + +```yaml +WAIT_FIELDS: 'cipeUrl,commitSha,cipeStatus' +LIGHT_FIELDS: 'cipeStatus,cipeUrl,branch,commitSha,selfHealingStatus,verificationStatus,userAction,failedTaskIds,verifiedTaskIds,selfHealingEnabled,failureClassification,couldAutoApplyTasks,autoApplySkipped,autoApplySkipReason,shortLink,confidence,confidenceReasoning,hints,selfHealingSkippedReason,selfHealingSkipMessage' +HEAVY_FIELDS: 'taskOutputSummary,suggestedFix,suggestedFixReasoning,suggestedFixDescription' +``` + +The `ci_information` tool accepts `branch` (optional, defaults to current git branch), `select` (comma-separated field names), and `pageToken` (0-based pagination for long strings). + +The `update_self_healing_fix` tool accepts a `shortLink` and an action: `APPLY`, `REJECT`, or `RERUN_ENVIRONMENT_STATE`. + +## Default Behaviors by Status + +The decision script returns one of the following statuses. This table defines the **default behavior** for each. User instructions can override any of these. + +**Simple exits** — just report and exit: + +| Status | Default Behavior | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------- | +| `ci_success` | Exit with success | +| `cipe_canceled` | Exit, CI was canceled | +| `cipe_timed_out` | Exit, CI timed out | +| `polling_timeout` | Exit, polling timeout reached | +| `circuit_breaker` | Exit, no progress after 5 consecutive polls | +| `environment_rerun_cap` | Exit, environment reruns exhausted | +| `fix_auto_applying` | Self-healing is handling it — just record `last_cipe_url`, enter wait mode. No MCP call or local git ops needed. | +| `error` | Wait 60s and loop | + +**Statuses requiring action** — when handling these in Step 3, read `references/fix-flows.md` for the detailed flow: + +| Status | Summary | +| ------------------------ | --------------------------------------------------------------------------------------------- | +| `fix_auto_apply_skipped` | Fix verified but auto-apply skipped (e.g., loop prevention). Inform user, offer manual apply. | +| `fix_apply_ready` | Fix verified (all tasks or e2e-only). Apply via MCP. | +| `fix_needs_local_verify` | Fix has unverified non-e2e tasks. Run locally, then apply or enhance. | +| `fix_needs_review` | Fix verification failed/not attempted. Analyze and decide. | +| `fix_failed` | Self-healing failed. Fetch heavy data, attempt local fix (gate check first). | +| `no_fix` | No fix available. Fetch heavy data, attempt local fix (gate check first) or exit. | +| `environment_issue` | Request environment rerun via MCP (gate check first). | +| `self_healing_throttled` | Reject old fixes, attempt local fix. | +| `no_new_cipe` | CI Attempt never spawned. Auto-fix workflow or exit with guidance. | +| `cipe_no_tasks` | CI failed with no tasks. Retry once with empty commit. | + +**Key rules (always apply):** + +- **Git safety**: Stage specific files by name — `git add -A` or `git add .` risks committing the user's unrelated work-in-progress or secrets +- **Environment failures** (OOM, command not found, permission denied): bail immediately. These aren't code bugs, so spending local-fix budget on them is wasteful +- **Gate check**: Run `ci-state-update.mjs gate` before local fix attempts — if budget exhausted, print message and exit + +## Main Loop + +### Step 1: Initialize Tracking + +``` +cycle_count = 0 # Only incremented for agent-initiated cycles (counted against --max-cycles) +start_time = now() +no_progress_count = 0 +local_verify_count = 0 +env_rerun_count = 0 +last_cipe_url = null +expected_commit_sha = null +agent_triggered = false # Set true after monitor takes an action that triggers new CI Attempt +poll_count = 0 +wait_mode = false +prev_status = null +prev_cipe_status = null +prev_sh_status = null +prev_verification_status = null +prev_failure_classification = null +``` + +### Step 2: Polling Loop + +Repeat until done: + +#### 2a. Spawn subagent (FETCH_STATUS) + +Determine select fields based on mode: + +- **Wait mode**: use WAIT_FIELDS (`cipeUrl,commitSha,cipeStatus`) +- **Normal mode (first poll or after newCipeDetected)**: use LIGHT_FIELDS + +Call the `ci_information` tool with the determined `select` fields for the current branch. Wait for the result before proceeding. + +#### 2b. Run decision script + +```bash +node /scripts/ci-poll-decide.mjs '' \ + [--wait-mode] \ + [--prev-cipe-url ] \ + [--expected-sha ] \ + [--prev-status ] \ + [--timeout ] \ + [--new-cipe-timeout ] \ + [--env-rerun-count ] \ + [--no-progress-count ] \ + [--prev-cipe-status ] \ + [--prev-sh-status ] \ + [--prev-verification-status ] \ + [--prev-failure-classification ] +``` + +The script outputs a single JSON line: `{ action, code, message, delay?, noProgressCount, envRerunCount, fields?, newCipeDetected?, verifiableTaskIds? }` + +#### 2c. Process script output + +Parse the JSON output and update tracking state: + +- `no_progress_count = output.noProgressCount` +- `env_rerun_count = output.envRerunCount` +- `prev_cipe_status = subagent_result.cipeStatus` +- `prev_sh_status = subagent_result.selfHealingStatus` +- `prev_verification_status = subagent_result.verificationStatus` +- `prev_failure_classification = subagent_result.failureClassification` +- `prev_status = output.action + ":" + (output.code || subagent_result.cipeStatus)` +- `poll_count++` + +Based on `action`: + +- **`action == "poll"`**: Print `output.message`, sleep `output.delay` seconds, go to 2a + - If `output.newCipeDetected`: clear wait mode, reset `wait_mode = false` +- **`action == "wait"`**: Print `output.message`, sleep `output.delay` seconds, go to 2a +- **`action == "done"`**: Proceed to Step 3 with `output.code` + +### Step 3: Handle Actionable Status + +When decision script returns `action == "done"`: + +1. Run cycle-check (Step 4) **before** handling the code +2. Check the returned `code` +3. Look up default behavior in the table above +4. Check if user instructions override the default +5. Execute the appropriate action +6. **If action expects new CI Attempt**, update tracking (see Step 3a) +7. If action results in looping, go to Step 2 + +#### Tool calls for actions + +Several statuses require fetching additional data or calling tools: + +- **fix_apply_ready**: Call `update_self_healing_fix` with action `APPLY` +- **fix_needs_local_verify**: Call `ci_information` with HEAVY_FIELDS for fix details before local verification +- **fix_needs_review**: Call `ci_information` with HEAVY_FIELDS → get `suggestedFixDescription`, `suggestedFixSummary`, `taskFailureSummaries` +- **fix_failed / no_fix**: Call `ci_information` with HEAVY_FIELDS → get `taskFailureSummaries` for local fix context +- **environment_issue**: Call `update_self_healing_fix` with action `RERUN_ENVIRONMENT_STATE` +- **self_healing_throttled**: Call `ci_information` with HEAVY_FIELDS → get `selfHealingSkipMessage`; then call `update_self_healing_fix` for each old fix + +### Step 3a: Track State for New-CI-Attempt Detection + +After actions that should trigger a new CI Attempt, run: + +```bash +node /scripts/ci-state-update.mjs post-action \ + --action \ + --cipe-url \ + --commit-sha +``` + +Action types: `fix-auto-applying`, `apply-mcp`, `apply-local-push`, `reject-fix-push`, `local-fix-push`, `env-rerun`, `auto-fix-push`, `empty-commit-push` + +The script returns `{ waitMode, pollCount, lastCipeUrl, expectedCommitSha, agentTriggered }`. Update all tracking state from the output, then go to Step 2. + +### Step 4: Cycle Classification and Progress Tracking + +When the decision script returns `action == "done"`, run cycle-check **before** handling the code: + +```bash +node /scripts/ci-state-update.mjs cycle-check \ + --code \ + [--agent-triggered] \ + --cycle-count --max-cycles \ + --env-rerun-count +``` + +The script returns `{ cycleCount, agentTriggered, envRerunCount, approachingLimit, message }`. Update tracking state from the output. + +- If `approachingLimit` → ask user whether to continue (with 5 or 10 more cycles) or stop monitoring +- If previous cycle was NOT agent-triggered (human pushed), log that human-initiated push was detected + +#### Progress Tracking + +- `no_progress_count`, circuit breaker (5 polls), and backoff reset are handled by ci-poll-decide.mjs (progress = any change in cipeStatus, selfHealingStatus, verificationStatus, or failureClassification) +- `env_rerun_count` reset on non-environment status is handled by ci-state-update.mjs cycle-check +- On new CI Attempt detected (poll script returns `newCipeDetected`) → reset `local_verify_count = 0`, `env_rerun_count = 0` + +## Error Handling + +| Error | Action | +| ------------------------------ | ----------------------------------------------------------------------------------------------------------- | +| Git rebase conflict | Report to user, exit | +| `nx-cloud apply-locally` fails | Reject fix via MCP (`action: "REJECT"`), then attempt manual patch (Reject + Fix From Scratch Flow) or exit | +| MCP tool error | Retry once, if fails report to user | +| Subagent spawn failure | Retry once, if fails exit with error | +| Decision script error | Treat as `error` status, increment `no_progress_count` | +| No new CI Attempt detected | If `--auto-fix-workflow`, try lockfile update; otherwise report to user with guidance | +| Lockfile auto-fix fails | Report to user, exit with guidance to check CI logs | + +## User Instruction Examples + +Users can override default behaviors: + +| Instruction | Effect | +| ------------------------------------------------ | --------------------------------------------------- | +| "never auto-apply" | Always prompt before applying any fix | +| "always ask before git push" | Prompt before each push | +| "reject any fix for e2e tasks" | Auto-reject if `failedTaskIds` contains e2e | +| "apply all fixes regardless of verification" | Skip verification check, apply everything | +| "if confidence < 70, reject" | Check confidence field before applying | +| "run 'nx affected -t typecheck' before applying" | Add local verification step | +| "auto-fix workflow failures" | Attempt lockfile updates on pre-CI-Attempt failures | +| "wait 45 min for new CI Attempt" | Override new-CI-Attempt timeout (default: 10 min) | diff --git a/.agents/skills/monitor-ci/references/fix-flows.md b/.agents/skills/monitor-ci/references/fix-flows.md new file mode 100644 index 0000000..b33aa02 --- /dev/null +++ b/.agents/skills/monitor-ci/references/fix-flows.md @@ -0,0 +1,108 @@ +# Detailed Status Handling & Fix Flows + +## Status Handling by Code + +### fix_auto_apply_skipped + +The script returns `autoApplySkipReason` in its output. + +1. Report the skip reason to the user (e.g., "Auto-apply was skipped because the previous CI pipeline execution was triggered by Nx Cloud") +2. Offer to apply the fix manually — spawn UPDATE_FIX subagent with `APPLY` if user agrees +3. Record `last_cipe_url`, enter wait mode + +### fix_apply_ready + +- Spawn UPDATE_FIX subagent with `APPLY` +- Record `last_cipe_url`, enter wait mode + +### fix_needs_local_verify + +The script returns `verifiableTaskIds` in its output. + +1. **Detect package manager:** `pnpm-lock.yaml` → `pnpm nx`, `yarn.lock` → `yarn nx`, otherwise `npx nx` +2. **Run verifiable tasks in parallel** — spawn `general` subagents for each task +3. **If all pass** → spawn UPDATE_FIX subagent with `APPLY`, enter wait mode +4. **If any fail** → Apply Locally + Enhance Flow (see below) + +### fix_needs_review + +Spawn FETCH_HEAVY subagent, then analyze fix content (`suggestedFixDescription`, `suggestedFixSummary`, `taskFailureSummaries`): + +- If fix looks correct → apply via MCP +- If fix needs enhancement → Apply Locally + Enhance Flow +- If fix is wrong → run `ci-state-update.mjs gate --gate-type local-fix`. If not allowed, print message and exit. Otherwise → Reject + Fix From Scratch Flow + +### fix_failed / no_fix + +Spawn FETCH_HEAVY subagent for `taskFailureSummaries`. Run `ci-state-update.mjs gate --gate-type local-fix` — if not allowed, print message and exit. Otherwise attempt local fix (counter already incremented by gate). If successful → commit, push, enter wait mode. If not → exit with failure. + +### environment_issue + +1. Run `ci-state-update.mjs gate --gate-type env-rerun`. If not allowed, print message and exit. +2. Spawn UPDATE_FIX subagent with `RERUN_ENVIRONMENT_STATE` +3. Enter wait mode with `last_cipe_url` set + +### self_healing_throttled + +Spawn FETCH_HEAVY subagent for `selfHealingSkipMessage`. + +1. **Parse throttle message** for CI Attempt URLs (regex: `/cipes/{id}`) +2. **Reject previous fixes** — for each URL: spawn FETCH_THROTTLE_INFO to get `shortLink`, then UPDATE_FIX with `REJECT` +3. **Attempt local fix**: Run `ci-state-update.mjs gate --gate-type local-fix`. If not allowed → skip to step 4. Otherwise use `failedTaskIds` and `taskFailureSummaries` for context. +4. **Fallback if local fix not possible or budget exhausted**: push empty commit (`git commit --allow-empty -m "ci: rerun after rejecting throttled fixes"`), enter wait mode + +### no_new_cipe + +1. Report to user: no CI attempt found, suggest checking CI provider +2. If `--auto-fix-workflow`: detect package manager, run install, commit lockfile if changed, enter wait mode +3. Otherwise: exit with guidance + +### cipe_no_tasks + +1. Report to user: CI failed with no tasks recorded +2. Retry: `git commit --allow-empty -m "chore: retry ci [monitor-ci]"` + push, enter wait mode +3. If retry also returns `cipe_no_tasks`: exit with failure + +## Fix Action Flows + +### Apply via MCP + +Spawn UPDATE_FIX subagent with `APPLY`. New CI Attempt spawns automatically. No local git ops. + +### Apply Locally + Enhance Flow + +1. `nx-cloud apply-locally ` (sets state to `APPLIED_LOCALLY`) +2. Enhance code to fix failing tasks +3. Run failing tasks to verify +4. If still failing → run `ci-state-update.mjs gate --gate-type local-fix`. If not allowed, commit current state and push (let CI be final judge). Otherwise loop back to enhance. +5. If passing → commit and push, enter wait mode + +### Reject + Fix From Scratch Flow + +1. Run `ci-state-update.mjs gate --gate-type local-fix`. If not allowed, print message and exit. +2. Spawn UPDATE_FIX subagent with `REJECT` +3. Fix from scratch locally +4. Commit and push, enter wait mode + +## Environment vs Code Failure Recognition + +When any local fix path runs a task and it fails, assess whether the failure is a **code issue** or an **environment/tooling issue** before running the gate script. + +**Indicators of environment/tooling failures** (non-exhaustive): command not found / binary missing, OOM / heap allocation failures, permission denied, network timeouts / DNS failures, missing system libraries, Docker/container issues, disk space exhaustion. + +When detected → bail immediately without running gate (no budget consumed). Report that the failure is an environment/tooling issue, not a code bug. + +**Code failures** (compilation errors, test assertion failures, lint violations, type errors) are genuine candidates for local fix attempts and proceed normally through the gate. + +## Git Safety + +- Stage specific files by name — `git add -A` or `git add .` risks committing the user's unrelated work-in-progress or secrets + +## Commit Message Format + +```bash +git commit -m "fix(): + +Failed tasks: , +Local verification: passed|enhanced|failed-pushing-to-ci" +``` diff --git a/.agents/skills/monitor-ci/scripts/ci-poll-decide.mjs b/.agents/skills/monitor-ci/scripts/ci-poll-decide.mjs new file mode 100644 index 0000000..3fe17e5 --- /dev/null +++ b/.agents/skills/monitor-ci/scripts/ci-poll-decide.mjs @@ -0,0 +1,428 @@ +#!/usr/bin/env node + +/** + * CI Poll Decision Script + * + * Deterministic decision engine for CI monitoring. + * Takes ci_information JSON + state args, outputs a single JSON action line. + * + * Architecture: + * classify() — pure decision tree, returns { action, code, extra? } + * buildOutput() — maps classification to full output with messages, delays, counters + * + * Usage: + * node ci-poll-decide.mjs '' \ + * [--wait-mode] [--prev-cipe-url ] [--expected-sha ] \ + * [--prev-status ] [--timeout ] [--new-cipe-timeout ] \ + * [--env-rerun-count ] [--no-progress-count ] \ + * [--prev-cipe-status ] [--prev-sh-status ] \ + * [--prev-verification-status ] [--prev-failure-classification ] + */ + +// --- Arg parsing --- + +const args = process.argv.slice(2); +const ciInfoJson = args[0]; +const pollCount = parseInt(args[1], 10) || 0; +const verbosity = args[2] || 'medium'; + +function getFlag(name) { + return args.includes(name); +} + +function getArg(name) { + const idx = args.indexOf(name); + return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null; +} + +const waitMode = getFlag('--wait-mode'); +const prevCipeUrl = getArg('--prev-cipe-url'); +const expectedSha = getArg('--expected-sha'); +const prevStatus = getArg('--prev-status'); +const timeoutSeconds = parseInt(getArg('--timeout') || '0', 10); +const newCipeTimeoutSeconds = parseInt(getArg('--new-cipe-timeout') || '0', 10); +const envRerunCount = parseInt(getArg('--env-rerun-count') || '0', 10); +const inputNoProgressCount = parseInt(getArg('--no-progress-count') || '0', 10); +const prevCipeStatus = getArg('--prev-cipe-status'); +const prevShStatus = getArg('--prev-sh-status'); +const prevVerificationStatus = getArg('--prev-verification-status'); +const prevFailureClassification = getArg('--prev-failure-classification'); + +// --- Parse CI info --- + +let ci; +try { + ci = JSON.parse(ciInfoJson); +} catch { + console.log( + JSON.stringify({ + action: 'done', + code: 'error', + message: 'Failed to parse ci_information JSON', + noProgressCount: inputNoProgressCount + 1, + envRerunCount, + }) + ); + process.exit(0); +} + +const { + cipeStatus, + selfHealingStatus, + verificationStatus, + selfHealingEnabled, + selfHealingSkippedReason, + failureClassification: rawFailureClassification, + failedTaskIds = [], + verifiedTaskIds = [], + couldAutoApplyTasks, + autoApplySkipped, + autoApplySkipReason, + userAction, + cipeUrl, + commitSha, +} = ci; + +const failureClassification = rawFailureClassification?.toLowerCase() ?? null; + +// --- Helpers --- + +function categorizeTasks() { + const verifiedSet = new Set(verifiedTaskIds); + const unverified = failedTaskIds.filter((t) => !verifiedSet.has(t)); + if (unverified.length === 0) return { category: 'all_verified' }; + + const e2e = unverified.filter((t) => { + const parts = t.split(':'); + return parts.length >= 2 && parts[1].includes('e2e'); + }); + if (e2e.length === unverified.length) return { category: 'e2e_only' }; + + const verifiable = unverified.filter((t) => { + const parts = t.split(':'); + return !(parts.length >= 2 && parts[1].includes('e2e')); + }); + return { category: 'needs_local_verify', verifiableTaskIds: verifiable }; +} + +function backoff(count) { + const delays = [60, 90, 120]; + return delays[Math.min(count, delays.length - 1)]; +} + +function hasStateChanged() { + if (prevCipeStatus && cipeStatus !== prevCipeStatus) return true; + if (prevShStatus && selfHealingStatus !== prevShStatus) return true; + if (prevVerificationStatus && verificationStatus !== prevVerificationStatus) + return true; + if ( + prevFailureClassification && + failureClassification !== prevFailureClassification + ) + return true; + return false; +} + +function isTimedOut() { + if (timeoutSeconds <= 0) return false; + const avgDelay = pollCount === 0 ? 0 : backoff(Math.floor(pollCount / 2)); + return pollCount * avgDelay >= timeoutSeconds; +} + +function isWaitTimedOut() { + if (newCipeTimeoutSeconds <= 0) return false; + return pollCount * 30 >= newCipeTimeoutSeconds; +} + +function isNewCipe() { + return ( + (prevCipeUrl && cipeUrl && cipeUrl !== prevCipeUrl) || + (expectedSha && commitSha && commitSha === expectedSha) + ); +} + +// ============================================================ +// classify() — pure decision tree +// +// Returns: { action: 'poll'|'wait'|'done', code: string, extra? } +// +// Decision priority (top wins): +// WAIT MODE: +// 1. new CI Attempt detected → poll (new_cipe_detected) +// 2. wait timed out → done (no_new_cipe) +// 3. still waiting → wait (waiting_for_cipe) +// NORMAL MODE: +// 4. polling timeout → done (polling_timeout) +// 5. circuit breaker (5 polls) → done (circuit_breaker) +// 6. CI succeeded → done (ci_success) +// 7. CI canceled → done (cipe_canceled) +// 8. CI timed out → done (cipe_timed_out) +// 9. CI failed, no tasks recorded → done (cipe_no_tasks) +// 10. environment failure → done (environment_rerun_cap | environment_issue) +// 11. self-healing throttled → done (self_healing_throttled) +// 12. CI in progress / not started → poll (ci_running) +// 13. self-healing in progress → poll (sh_running) +// 14. flaky task auto-rerun → poll (flaky_rerun) +// 15. fix auto-applied → poll (fix_auto_applied) +// 16. auto-apply: skipped → done (fix_auto_apply_skipped) +// 17. auto-apply: verification pending→ poll (verification_pending) +// 18. auto-apply: verified → done (fix_auto_applying) +// 19. fix: verification failed/none → done (fix_needs_review) +// 20. fix: all/e2e verified → done (fix_apply_ready) +// 21. fix: needs local verify → done (fix_needs_local_verify) +// 22. self-healing failed → done (fix_failed) +// 23. no fix available → done (no_fix) +// 24. fallback → poll (fallback) +// ============================================================ + +function classify() { + // --- Wait mode --- + if (waitMode) { + if (isNewCipe()) return { action: 'poll', code: 'new_cipe_detected' }; + if (isWaitTimedOut()) return { action: 'done', code: 'no_new_cipe' }; + return { action: 'wait', code: 'waiting_for_cipe' }; + } + + // --- Guards --- + if (isTimedOut()) return { action: 'done', code: 'polling_timeout' }; + if (noProgressCount >= 5) return { action: 'done', code: 'circuit_breaker' }; + + // --- Terminal CI states --- + if (cipeStatus === 'SUCCEEDED') return { action: 'done', code: 'ci_success' }; + if (cipeStatus === 'CANCELED') + return { action: 'done', code: 'cipe_canceled' }; + if (cipeStatus === 'TIMED_OUT') + return { action: 'done', code: 'cipe_timed_out' }; + + // --- CI failed, no tasks --- + if ( + cipeStatus === 'FAILED' && + failedTaskIds.length === 0 && + selfHealingStatus == null + ) + return { action: 'done', code: 'cipe_no_tasks' }; + + // --- Environment failure --- + if (failureClassification === 'environment_state') { + if (envRerunCount >= 2) + return { action: 'done', code: 'environment_rerun_cap' }; + return { action: 'done', code: 'environment_issue' }; + } + + // --- Throttled --- + if (selfHealingSkippedReason === 'THROTTLED') + return { action: 'done', code: 'self_healing_throttled' }; + + // --- Still running: CI --- + if (cipeStatus === 'IN_PROGRESS' || cipeStatus === 'NOT_STARTED') + return { action: 'poll', code: 'ci_running' }; + + // --- Still running: self-healing --- + if ( + (selfHealingStatus === 'IN_PROGRESS' || + selfHealingStatus === 'NOT_STARTED') && + !selfHealingSkippedReason + ) + return { action: 'poll', code: 'sh_running' }; + + // --- Still running: flaky rerun --- + if (failureClassification === 'flaky_task') + return { action: 'poll', code: 'flaky_rerun' }; + + // --- Fix auto-applied, waiting for new CI Attempt --- + if (userAction === 'APPLIED_AUTOMATICALLY') + return { action: 'poll', code: 'fix_auto_applied' }; + + // --- Auto-apply path (couldAutoApplyTasks) --- + if (couldAutoApplyTasks === true) { + if (autoApplySkipped === true) + return { + action: 'done', + code: 'fix_auto_apply_skipped', + extra: { autoApplySkipReason }, + }; + if ( + verificationStatus === 'NOT_STARTED' || + verificationStatus === 'IN_PROGRESS' + ) + return { action: 'poll', code: 'verification_pending' }; + if (verificationStatus === 'COMPLETED') + return { action: 'done', code: 'fix_auto_applying' }; + // verification FAILED or NOT_EXECUTABLE → falls through to fix_needs_review + } + + // --- Fix available --- + if (selfHealingStatus === 'COMPLETED') { + if ( + verificationStatus === 'FAILED' || + verificationStatus === 'NOT_EXECUTABLE' || + (couldAutoApplyTasks !== true && !verificationStatus) + ) + return { action: 'done', code: 'fix_needs_review' }; + + const tasks = categorizeTasks(); + if (tasks.category === 'all_verified' || tasks.category === 'e2e_only') + return { action: 'done', code: 'fix_apply_ready' }; + return { + action: 'done', + code: 'fix_needs_local_verify', + extra: { verifiableTaskIds: tasks.verifiableTaskIds }, + }; + } + + // --- Fix failed --- + if (selfHealingStatus === 'FAILED') + return { action: 'done', code: 'fix_failed' }; + + // --- No fix available --- + if ( + cipeStatus === 'FAILED' && + (selfHealingEnabled === false || selfHealingStatus === 'NOT_EXECUTABLE') + ) + return { action: 'done', code: 'no_fix' }; + + // --- Fallback --- + return { action: 'poll', code: 'fallback' }; +} + +// ============================================================ +// buildOutput() — maps classification to full JSON output +// ============================================================ + +// Message templates keyed by status or key +const messages = { + // wait mode + new_cipe_detected: () => + `New CI Attempt detected! CI: ${cipeStatus || 'N/A'}`, + no_new_cipe: () => + 'New CI Attempt timeout exceeded. No new CI Attempt detected.', + waiting_for_cipe: () => 'Waiting for new CI Attempt...', + + // guards + polling_timeout: () => 'Polling timeout exceeded.', + circuit_breaker: () => 'No progress after 5 consecutive polls. Stopping.', + + // terminal + ci_success: () => 'CI passed successfully!', + cipe_canceled: () => 'CI Attempt was canceled.', + cipe_timed_out: () => 'CI Attempt timed out.', + cipe_no_tasks: () => 'CI failed but no Nx tasks were recorded.', + + // environment + environment_rerun_cap: () => 'Environment rerun cap (2) exceeded. Bailing.', + environment_issue: () => 'CI: FAILED | Classification: ENVIRONMENT_STATE', + + // throttled + self_healing_throttled: () => + 'Self-healing throttled \u2014 too many unapplied fixes.', + + // polling + ci_running: () => `CI: ${cipeStatus}`, + sh_running: () => `CI: ${cipeStatus} | Self-healing: ${selfHealingStatus}`, + flaky_rerun: () => + 'CI: FAILED | Classification: FLAKY_TASK (auto-rerun in progress)', + fix_auto_applied: () => + 'CI: FAILED | Fix auto-applied, new CI Attempt spawning', + verification_pending: () => + `CI: FAILED | Self-healing: COMPLETED | Verification: ${verificationStatus}`, + + // actionable + fix_auto_applying: () => 'Fix verified! Auto-applying...', + fix_auto_apply_skipped: (extra) => + `Fix verified but auto-apply was skipped. ${ + extra?.autoApplySkipReason + ? `Reason: ${extra.autoApplySkipReason}` + : 'Offer to apply manually.' + }`, + fix_needs_review: () => + `Fix available but needs review. Verification: ${ + verificationStatus || 'N/A' + }`, + fix_apply_ready: () => 'Fix available and verified. Ready to apply.', + fix_needs_local_verify: (extra) => + `Fix available. ${extra.verifiableTaskIds.length} task(s) need local verification.`, + fix_failed: () => 'Self-healing failed to generate a fix.', + no_fix: () => 'CI failed, no fix available.', + + // fallback + fallback: () => + `CI: ${cipeStatus || 'N/A'} | Self-healing: ${ + selfHealingStatus || 'N/A' + } | Verification: ${verificationStatus || 'N/A'}`, +}; + +// Codes where noProgressCount resets to 0 (genuine progress occurred) +const resetProgressCodes = new Set([ + 'ci_success', + 'fix_auto_applying', + 'fix_auto_apply_skipped', + 'fix_needs_review', + 'fix_apply_ready', + 'fix_needs_local_verify', +]); + +function formatMessage(msg) { + if (verbosity === 'minimal') { + const currentStatus = `${cipeStatus}|${selfHealingStatus}|${verificationStatus}`; + if (currentStatus === (prevStatus || '')) return null; + return msg; + } + if (verbosity === 'verbose') { + return [ + `Poll #${pollCount + 1} | CI: ${cipeStatus || 'N/A'} | Self-healing: ${ + selfHealingStatus || 'N/A' + } | Verification: ${verificationStatus || 'N/A'}`, + msg, + ].join('\n'); + } + return `Poll #${pollCount + 1} | ${msg}`; +} + +function buildOutput(decision) { + const { action, code, extra } = decision; + + // noProgressCount is already computed before classify() was called. + // Here we only handle the reset for "genuine progress" done-codes. + + const msgFn = messages[code]; + const rawMsg = msgFn ? msgFn(extra) : `Unknown: ${code}`; + const message = formatMessage(rawMsg); + + const result = { + action, + code, + message, + noProgressCount: resetProgressCodes.has(code) ? 0 : noProgressCount, + envRerunCount, + }; + + // Add delay + if (action === 'wait') { + result.delay = 30; + } else if (action === 'poll') { + result.delay = code === 'new_cipe_detected' ? 60 : backoff(noProgressCount); + result.fields = 'light'; + } + + // Add extras + if (code === 'new_cipe_detected') result.newCipeDetected = true; + if (extra?.verifiableTaskIds) + result.verifiableTaskIds = extra.verifiableTaskIds; + if (extra?.autoApplySkipReason) + result.autoApplySkipReason = extra.autoApplySkipReason; + + console.log(JSON.stringify(result)); +} + +// --- Run --- + +// Compute noProgressCount from input. Single assignment, no mutation. +// Wait mode: reset on new cipe, otherwise unchanged (wait doesn't count as no-progress). +// Normal mode: reset on any state change, otherwise increment. +const noProgressCount = (() => { + if (waitMode) return isNewCipe() ? 0 : inputNoProgressCount; + if (isNewCipe() || hasStateChanged()) return 0; + return inputNoProgressCount + 1; +})(); + +buildOutput(classify()); diff --git a/.agents/skills/monitor-ci/scripts/ci-state-update.mjs b/.agents/skills/monitor-ci/scripts/ci-state-update.mjs new file mode 100644 index 0000000..90fa714 --- /dev/null +++ b/.agents/skills/monitor-ci/scripts/ci-state-update.mjs @@ -0,0 +1,160 @@ +#!/usr/bin/env node + +/** + * CI State Update Script + * + * Deterministic state management for CI monitor actions. + * Three commands: gate, post-action, cycle-check. + * + * Usage: + * node ci-state-update.mjs gate --gate-type [counter args] + * node ci-state-update.mjs post-action --action [--cipe-url ] [--commit-sha ] + * node ci-state-update.mjs cycle-check --code [--agent-triggered] [counter args] + */ + +// --- Arg parsing --- + +const args = process.argv.slice(2); +const command = args[0]; + +function getFlag(name) { + return args.includes(name); +} + +function getArg(name) { + const idx = args.indexOf(name); + return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : null; +} + +function output(result) { + console.log(JSON.stringify(result)); +} + +// --- gate --- +// Check if an action is allowed and return incremented counter. +// Called before any local fix attempt or environment rerun. + +function gate() { + const gateType = getArg('--gate-type'); + + if (gateType === 'local-fix') { + const count = parseInt(getArg('--local-verify-count') || '0', 10); + const max = parseInt(getArg('--local-verify-attempts') || '3', 10); + if (count >= max) { + return output({ + allowed: false, + localVerifyCount: count, + message: `Local fix budget exhausted (${count}/${max} attempts)`, + }); + } + return output({ + allowed: true, + localVerifyCount: count + 1, + message: null, + }); + } + + if (gateType === 'env-rerun') { + const count = parseInt(getArg('--env-rerun-count') || '0', 10); + if (count >= 2) { + return output({ + allowed: false, + envRerunCount: count, + message: `Environment issue persists after ${count} reruns. Manual investigation needed.`, + }); + } + return output({ + allowed: true, + envRerunCount: count + 1, + message: null, + }); + } + + output({ allowed: false, message: `Unknown gate type: ${gateType}` }); +} + +// --- post-action --- +// Compute next state after an action is taken. +// Returns wait mode params and whether the action was agent-triggered. + +function postAction() { + const action = getArg('--action'); + const cipeUrl = getArg('--cipe-url'); + const commitSha = getArg('--commit-sha'); + + // MCP-triggered or auto-applied: track by cipeUrl + const cipeUrlActions = ['fix-auto-applying', 'apply-mcp', 'env-rerun']; + // Local push: track by commitSha + const commitShaActions = [ + 'apply-local-push', + 'reject-fix-push', + 'local-fix-push', + 'auto-fix-push', + 'empty-commit-push', + ]; + + const trackByCipeUrl = cipeUrlActions.includes(action); + const trackByCommitSha = commitShaActions.includes(action); + + if (!trackByCipeUrl && !trackByCommitSha) { + return output({ error: `Unknown action: ${action}` }); + } + + // fix-auto-applying: self-healing did it, NOT the monitor + const agentTriggered = action !== 'fix-auto-applying'; + + output({ + waitMode: true, + pollCount: 0, + lastCipeUrl: trackByCipeUrl ? cipeUrl : null, + expectedCommitSha: trackByCommitSha ? commitSha : null, + agentTriggered, + }); +} + +// --- cycle-check --- +// Cycle classification + counter resets when a new "done" code is received. +// Called at the start of handling each actionable code. + +function cycleCheck() { + const status = getArg('--code'); + const wasAgentTriggered = getFlag('--agent-triggered'); + let cycleCount = parseInt(getArg('--cycle-count') || '0', 10); + const maxCycles = parseInt(getArg('--max-cycles') || '10', 10); + let envRerunCount = parseInt(getArg('--env-rerun-count') || '0', 10); + + // Cycle classification: if previous cycle was agent-triggered, count it + if (wasAgentTriggered) cycleCount++; + + // Reset env_rerun_count on non-environment status + if (status !== 'environment_issue') envRerunCount = 0; + + // Approaching limit gate + const approachingLimit = cycleCount >= maxCycles - 2; + + output({ + cycleCount, + agentTriggered: false, + envRerunCount, + approachingLimit, + message: approachingLimit + ? `Approaching cycle limit (${cycleCount}/${maxCycles})` + : null, + }); +} + +// --- Dispatch --- + +switch (command) { + case 'gate': + gate(); + break; + case 'post-action': + postAction(); + break; + case 'cycle-check': + cycleCheck(); + break; + default: + output({ error: `Unknown command: ${command}` }); +} diff --git a/.agents/skills/nx-generate/SKILL.md b/.agents/skills/nx-generate/SKILL.md new file mode 100644 index 0000000..af7ba80 --- /dev/null +++ b/.agents/skills/nx-generate/SKILL.md @@ -0,0 +1,166 @@ +--- +name: nx-generate +description: Generate code using nx generators. INVOKE IMMEDIATELY when user mentions scaffolding, setup, structure, creating apps/libs, or setting up project structure. Trigger words - scaffold, setup, create a ... app, create a ... lib, project structure, generate, add a new project. ALWAYS use this BEFORE calling nx_docs or exploring - this skill handles discovery internally. +--- + +# Run Nx Generator + +Nx generators are powerful tools that scaffold projects, make automated code migrations or automate repetitive tasks in a monorepo. They ensure consistency across the codebase and reduce boilerplate work. + +This skill applies when the user wants to: + +- Create new projects like libraries or applications +- Scaffold features or boilerplate code +- Run workspace-specific or custom generators +- Do anything else that an nx generator exists for + +## Key Principles + +1. **Always use `--no-interactive`** - Prevents prompts that would hang execution +2. **Read the generator source code** - The schema alone is not enough; understand what the generator actually does +3. **Match existing repo patterns** - Study similar artifacts in the repo and follow their conventions +4. **Verify with lint/test/build/typecheck etc.** - Generated code must pass verification. The listed targets are just an example, use what's appropriate for this workspace. + +## Steps + +### 1. Discover Available Generators + +Use the Nx CLI to discover available generators: + +- List all generators for a plugin: `npx nx list @nx/react` +- View available plugins: `npx nx list` + +This includes plugin generators (e.g., `@nx/react:library`) and local workspace generators. + +### 2. Match Generator to User Request + +Identify which generator(s) could fulfill the user's needs. Consider what artifact type they want, which framework is relevant, and any specific generator names mentioned. + +**IMPORTANT**: When both a local workspace generator and an external plugin generator could satisfy the request, **always prefer the local workspace generator**. Local generators are customized for the specific repo's patterns. + +If no suitable generator exists, you can stop using this skill. However, the burden of proof is high—carefully consider all available generators before deciding none apply. + +### 3. Get Generator Options + +Use the `--help` flag to understand available options: + +```bash +npx nx g @nx/react:library --help +``` + +Pay attention to required options, defaults that might need overriding, and options relevant to the user's request. + +### Library Buildability + +**Default to non-buildable libraries** unless there's a specific reason for buildable. + +| Type | When to use | Generator flags | +| --------------------------- | ----------------------------------------------------------------- | ----------------------------------- | +| **Non-buildable** (default) | Internal monorepo libs consumed by apps | No `--bundler` flag | +| **Buildable** | Publishing to npm, cross-repo sharing, stable libs for cache hits | `--bundler=vite` or `--bundler=swc` | + +Non-buildable libs: + +- Export `.ts`/`.tsx` source directly +- Consumer's bundler compiles them +- Faster dev experience, less config + +Buildable libs: + +- Have their own build target +- Useful for stable libs that rarely change (cache hits) +- Required for npm publishing + +**If unclear, ask the user:** "Should this library be buildable (own build step, better caching) or non-buildable (source consumed directly, simpler setup)?" + +### 4. Read Generator Source Code + +**This step is critical.** The schema alone does not tell you everything. Reading the source code helps you: + +- Know exactly what files will be created/modified and where +- Understand side effects (updating configs, installing deps, etc.) +- Identify behaviors and options not obvious from the schema +- Understand how options interact with each other + +To find generator source code: + +- For plugin generators: Use `node -e "console.log(require.resolve('@nx//generators.json'));"` to find the generators.json, then locate the source from there +- If that fails, read directly from `node_modules//generators.json` +- For local generators: Typically in `tools/generators/` or a local plugin directory. Search the repo for the generator name. + +After reading the source, reconsider: Is this the right generator? If not, go back to step 2. + +> **⚠️ `--directory` flag behavior can be misleading.** +> It should specify the full path of the generated library or component, not the parent path that it will be generated in. +> +> ```bash +> # ✅ Correct - directory is the full path for the library +> nx g @nx/react:library --directory=libs/my-lib +> # generates libs/my-lib/package.json and more +> +> # ❌ Wrong - this will create files at libs and libs/src/... +> nx g @nx/react:library --name=my-lib --directory=libs +> # generates libs/package.json and more +> ``` + +### 5. Examine Existing Patterns + +Before generating, examine the target area of the codebase: + +- Look at similar existing artifacts (other libraries, applications, etc.) +- Identify naming conventions, file structures, and configuration patterns +- Note which test runners, build tools, and linters are used +- Configure the generator to match these patterns + +### 6. Dry-Run to Verify File Placement + +**Always run with `--dry-run` first** to verify files will be created in the correct location: + +```bash +npx nx g @nx/react:library --name=my-lib --dry-run --no-interactive +``` + +Review the output carefully. If files would be created in the wrong location, adjust your options based on what you learned from the generator source code. + +Note: Some generators don't support dry-run (e.g., if they install npm packages). If dry-run fails for this reason, proceed to running the generator for real. + +### 7. Run the Generator + +Execute the generator: + +```bash +nx generate --no-interactive +``` + +> **Tip:** New packages often need workspace dependencies wired up (e.g., importing shared types, being consumed by apps). The `link-workspace-packages` skill can help add these correctly. + +### 8. Modify Generated Code (If Needed) + +Generators provide a starting point. Modify the output as needed to: + +- Add or modify functionality as requested +- Adjust imports, exports, or configurations +- Integrate with existing code patterns + +**Important:** If you replace or delete generated test files (e.g., `*.spec.ts`), either write meaningful replacement tests or remove the `test` target from the project configuration. Empty test suites will cause `nx test` to fail. + +### 9. Format and Verify + +Format all generated/modified files: + +```bash +nx format --fix +``` + +This example is for built-in nx formatting with prettier. There might be other formatting tools for this workspace, use these when appropriate. + +Then verify the generated code works. Keep in mind that the changes you make with a generator or subsequent modifications might impact various projects so it's usually not enough to only run targets for the artifact you just created. + +```bash +# these targets are just an example! +nx run-many -t build,lint,test,typecheck +``` + +These targets are common examples used across many workspaces. You should do research into other targets available for this workspace and its projects. CI configuration is usually a good guide for what the critical targets are that have to pass. + +If verification fails with manageable issues (a few lint errors, minor type issues), fix them. If issues are extensive, attempt obvious fixes first, then escalate to the user with details about what was generated, what's failing, and what you've attempted. diff --git a/.agents/skills/nx-import/SKILL.md b/.agents/skills/nx-import/SKILL.md new file mode 100644 index 0000000..b1cd381 --- /dev/null +++ b/.agents/skills/nx-import/SKILL.md @@ -0,0 +1,238 @@ +--- +name: nx-import +description: Import, merge, or combine repositories into an Nx workspace using nx import. USE WHEN the user asks to adopt Nx across repos, move projects into a monorepo, or bring code/history from another repository. +--- + +## Quick Start + +- `nx import` brings code from a source repository or folder into the current workspace, preserving commit history. +- After nx `22.6.0`, `nx import` responds with .ndjson outputs and follow-up questions. For earlier versions, always run with `--no-interactive` and specify all flags directly. +- Run `nx import --help` for available options. +- Make sure the destination directory is empty before importing. + EXAMPLE: target has `libs/utils` and `libs/models`; source has `libs/ui` and `libs/data-access` — you cannot import `libs/` into `libs/` directly. Import each source library individually. + +Primary docs: + +- https://nx.dev/docs/guides/adopting-nx/import-project +- https://nx.dev/docs/guides/adopting-nx/preserving-git-histories + +Read the nx docs if you have the tools for it. + +## Import Strategy + +**Subdirectory-at-a-time** (`nx import apps --source=apps`): + +- **Recommended for monorepo sources** — files land at top level, no redundant config +- Caveats: multiple import commands (separate merge commits each); dest must not have conflicting directories; root configs (deps, plugins, targetDefaults) not imported +- **Directory conflicts**: Import into alternate-named dir (e.g. `imported-apps/`), then rename + +**Whole repo** (`nx import imported --source=.`): + +- **Only for non-monorepo sources** (single-project repos) +- For monorepos, creates messy nested config (`imported/nx.json`, `imported/tsconfig.base.json`, etc.) +- If you must: keep imported `tsconfig.base.json` (projects extend it), prefix workspace globs and executor paths + +### Directory Conventions + +- **Always prefer the destination's existing conventions.** Source uses `libs/`but dest uses `packages/`? Import into `packages/` (`nx import packages/foo --source=libs/foo`). +- If dest has no convention (empty workspace), ask the user. + +### Application vs Library Detection + +Before importing, identify whether the source is an **application** or a **library**: + +- **Applications**: Deployable end products. Common indicators: + - _Frontend_: `next.config.*`, `vite.config.*` with a build entry point, framework-specific app scaffolding (CRA, Angular CLI app, etc.) + - _Backend (Node.js)_: Express/Fastify/NestJS server entrypoint, no `"exports"` field in `package.json` + - _JVM_: Maven `pom.xml` with `jar` or `war` and a `main` class; Gradle `application` plugin or `mainClass` setting + - _.NET_: `.csproj`/`.fsproj` with `Exe` or `WinExe` + - _General_: Dockerfile, a runnable entrypoint, no public API surface intended for import by other projects +- **Libraries**: Reusable packages consumed by other projects. Common indicators: `"main"`/`"exports"` in `package.json`, Maven/Gradle packaging as a library jar, .NET `Library`, named exports intended for import by other packages. + +**Destination directory rules**: + +- Applications → `apps/`. Check workspace globs (e.g. `pnpm-workspace.yaml`, `workspaces` in root `package.json`) for an existing `apps/*` entry. + - If `apps/*` is **not** present, add it before importing: update the workspace glob config and commit (or stage) the change. + - Example: `nx import apps/my-app --source=packages/my-app` +- Libraries → follow the dest's existing convention (`packages/`, `libs/`, etc.). + +## Common Issues + +### pnpm Workspace Globs (Critical) + +`nx import` adds the imported directory itself (e.g. `apps`) to `pnpm-workspace.yaml`, **NOT** glob patterns for packages within it. Cross-package imports will fail with `Cannot find module`. + +**Fix**: Replace with proper globs from the source config (e.g. `apps/*`, `libs/shared/*`), then `pnpm install`. + +### Root Dependencies and Config Not Imported (Critical) + +`nx import` does **NOT** merge from the source's root: + +- `dependencies`/`devDependencies` from `package.json` +- `targetDefaults` from `nx.json` (e.g. `"@nx/esbuild:esbuild": { "dependsOn": ["^build"] }` — critical for build ordering) +- `namedInputs` from `nx.json` (e.g. `production` exclusion patterns for test files) +- Plugin configurations from `nx.json` + +**Fix**: Diff source and dest `package.json` + `nx.json`. Add missing deps, merge relevant `targetDefaults` and `namedInputs`. + +### TypeScript Project References + +After import, run `nx sync --yes`. If it reports nothing but typecheck still fails, `nx reset` first, then `nx sync --yes` again. + +### Explicit Executor Path Fixups + +Inferred targets (via Nx plugins) resolve config relative to project root — no changes needed. Explicit executor targets (e.g. `@nx/esbuild:esbuild`) have workspace-root-relative paths (`main`, `outputPath`, `tsConfig`, `assets`, `sourceRoot`) that must be prefixed with the import destination directory. + +### Plugin Detection + +- **Whole-repo import**: `nx import` detects and offers to install plugins. Accept them. +- **Subdirectory import**: Plugins NOT auto-detected. Manually add with `npx nx add @nx/PLUGIN`. Check `include`/`exclude` patterns — defaults won't match alternate directories (e.g. `apps-beta/`). +- Run `npx nx reset` after any plugin config changes. + +### Redundant Root Files (Whole-Repo Only) + +Whole-repo import brings ALL source root files into the dest subdirectory. Clean up: + +- `pnpm-lock.yaml` — stale; dest has its own lockfile +- `pnpm-workspace.yaml` — source workspace config; conflicts with dest +- `node_modules/` — stale symlinks pointing to source filesystem +- `.gitignore` — redundant with dest root `.gitignore` +- `nx.json` — source Nx config; dest has its own +- `README.md` — optional; keep or remove + +**Don't blindly delete** `tsconfig.base.json` — imported projects may extend it via relative paths. + +### Root ESLint Config Missing (Subdirectory Import) + +Subdirectory import doesn't bring the source's root `eslint.config.mjs`, but project configs reference `../../eslint.config.mjs`. + +**Fix order**: + +1. Install ESLint deps first: `pnpm add -wD eslint@^9 @nx/eslint-plugin typescript-eslint` (plus framework-specific plugins) +2. Create root `eslint.config.mjs` (copy from source or create with `@nx/eslint-plugin` base rules) +3. Then `npx nx add @nx/eslint` to register the plugin in `nx.json` + +Install `typescript-eslint` explicitly — pnpm's strict hoisting won't auto-resolve this transitive dep of `@nx/eslint-plugin`. + +### ESLint Version Pinning (Critical) + +**Pin ESLint to v9** (`eslint@^9.0.0`). ESLint 10 breaks `@nx/eslint` and many plugins with cryptic errors like `Cannot read properties of undefined (reading 'version')`. + +`@nx/eslint` may peer-depend on ESLint 8, causing the wrong version to resolve. If lint fails with `Cannot read properties of undefined (reading 'allow')`, add `pnpm.overrides`: + +```json +{ "pnpm": { "overrides": { "eslint": "^9.0.0" } } } +``` + +### Dependency Version Conflicts + +After import, compare key deps (`typescript`, `eslint`, framework-specific). If dest uses newer versions, upgrade imported packages to match (usually safe). If source is newer, may need to upgrade dest first. Use `pnpm.overrides` to enforce single-version policy if desired. + +### Module Boundaries + +Imported projects may lack `tags`. Add tags or update `@nx/enforce-module-boundaries` rules. + +### Project Name Collisions (Multi-Import) + +Same `name` in `package.json` across source and dest causes `MultipleProjectsWithSameNameError`. **Fix**: Rename conflicting names (e.g. `@org/api` → `@org/teama-api`), update all dep references and import statements, `pnpm install`. The root `package.json` of each imported repo also becomes a project — rename those too. + +### Workspace Dep Import Ordering + +`pnpm install` fails during `nx import` if a `"workspace:*"` dependency hasn't been imported yet. File operations still succeed. **Fix**: Import all projects first, then `pnpm install --no-frozen-lockfile`. + +### `.gitkeep` Blocking Subdirectory Import + +The TS preset creates `packages/.gitkeep`. Remove it and commit before importing. + +### Frontend tsconfig Base Settings (Critical) + +The TS preset defaults (`module: "nodenext"`, `moduleResolution: "nodenext"`, `lib: ["es2022"]`) are incompatible with frontend frameworks (React, Next.js, Vue, Vite). After importing frontend projects, verify the dest root `tsconfig.base.json`: + +- **`moduleResolution`**: Must be `"bundler"` (not `"nodenext"`) +- **`module`**: Must be `"esnext"` (not `"nodenext"`) +- **`lib`**: Must include `"dom"` and `"dom.iterable"` (frontend projects need these) +- **`jsx`**: `"react-jsx"` for React-only workspaces, per-project for mixed frameworks + +For **subdirectory imports**, the dest root tsconfig is authoritative — update it. For **whole-repo imports**, imported projects may extend their own nested `tsconfig.base.json`, making this less critical. + +If the dest also has backend projects needing `nodenext`, use per-project overrides instead of changing the root. + +**Gotcha**: TypeScript does NOT merge `lib` arrays — a project-level override **replaces** the base array entirely. Always include all needed entries (e.g. `es2022`, `dom`, `dom.iterable`) in any project-level `lib`. + +### `@nx/react` Typings for Libraries + +React libraries generated with `@nx/react:library` reference `@nx/react/typings/cssmodule.d.ts` and `@nx/react/typings/image.d.ts` in their tsconfig `types`. These fail with `Cannot find type definition file` unless `@nx/react` is installed in the dest workspace. + +**Fix**: `pnpm add -wD @nx/react` + +### Jest Preset Missing (Subdirectory Import) + +Nx presets create `jest.preset.js` at the workspace root, and project jest configs reference it (e.g. `../../jest.preset.js`). Subdirectory import does NOT bring this file. + +**Fix**: + +1. Run `npx nx add @nx/jest` — registers `@nx/jest/plugin` in `nx.json` and updates `namedInputs` +2. Create `jest.preset.js` at workspace root (see `references/JEST.md` for content) — `nx add` only creates this when a generator runs, not on bare `nx add` +3. Install test runner deps: `pnpm add -wD jest jest-environment-jsdom ts-jest @types/jest` +4. Install framework-specific test deps as needed (see `references/JEST.md`) + +For deeper Jest issues (tsconfig.spec.json, Babel transforms, CI atomization, Jest vs Vitest coexistence), see `references/JEST.md`. + +### Target Name Prefixing (Whole-Repo Import) + +When importing a project with existing npm scripts (`build`, `dev`, `start`, `lint`), Nx plugins auto-prefix inferred target names to avoid conflicts: e.g. `next:build`, `vite:build`, `eslint:lint`. + +**Fix**: Remove the Nx-rewritten npm scripts from the imported `package.json`, then either: + +- Accept the prefixed names (e.g. `nx run app:next:build`) +- Rename plugin target names in `nx.json` to use unprefixed names + +## Non-Nx Source Issues + +When the source is a plain pnpm/npm workspace without `nx.json`. + +### npm Script Rewriting (Critical) + +Nx rewrites `package.json` scripts during init, creating broken commands (e.g. `vitest run` → `nx test run`). **Fix**: Remove all rewritten scripts — Nx plugins infer targets from config files. + +### `noEmit` → `composite` + `emitDeclarationOnly` (Critical) + +Plain TS projects use `"noEmit": true`, incompatible with Nx project references. + +**Symptoms**: "typecheck target is disabled because one or more project references set 'noEmit: true'" or TS6310. + +**Fix** in **all** imported tsconfigs: + +1. Remove `"noEmit": true`. If inherited via extends chain, set `"noEmit": false` explicitly. +2. Add `"composite": true`, `"emitDeclarationOnly": true`, `"declarationMap": true` +3. Add `"outDir": "dist"` and `"tsBuildInfoFile": "dist/tsconfig.tsbuildinfo"` +4. Add `"extends": "../../tsconfig.base.json"` if missing. Remove settings now inherited from base. + +### Stale node_modules and Lockfiles + +`nx import` may bring `node_modules/` (pnpm symlinks pointing to the source filesystem) and `pnpm-lock.yaml` from the source. Both are stale. + +**Fix**: `rm -rf imported/node_modules imported/pnpm-lock.yaml imported/pnpm-workspace.yaml imported/.gitignore`, then `pnpm install`. + +### ESLint Config Handling + +- **Legacy `.eslintrc.json` (ESLint 8)**: Delete all `.eslintrc.*`, remove v8 deps, create flat `eslint.config.mjs`. +- **Flat config (`eslint.config.js`)**: Self-contained configs can often be left as-is. +- **No ESLint**: Create both root and project-level configs from scratch. + +### TypeScript `paths` Aliases + +Nx uses `package.json` `"exports"` + pnpm workspace linking instead of tsconfig `"paths"`. If packages have proper `"exports"`, paths are redundant. Otherwise, update paths for the new directory structure. + +## Technology-specific Guidance + +Identify technologies in the source repo, then read and apply the matching reference file(s). + +Available references: + +- `references/ESLINT.md` — ESLint projects: duplicate `lint`/`eslint:lint` targets, legacy `.eslintrc.*` linting generated files, flat config `.cjs` self-linting, `typescript-eslint` v7/v9 peer dep conflict, mixed ESLint v8+v9 in one workspace. +- `references/GRADLE.md` +- `references/JEST.md` — Jest testing: `@nx/jest/plugin` setup, jest.preset.js, testing deps by framework, tsconfig.spec.json, Jest vs Vitest coexistence, Babel transforms, CI atomization. +- `references/NEXT.md` — Next.js projects: `@nx/next/plugin` targets, `withNx`, Next.js TS config (`noEmit`, `jsx: "preserve"`), auto-installing deps via wrong PM, non-Nx `create-next-app` imports, mixed Next.js+Vite coexistence. +- `references/TURBOREPO.md` +- `references/VITE.md` — Vite projects (React, Vue, or both): `@nx/vite/plugin` typecheck target, `resolve.alias`/`__dirname` fixes, framework deps, Vue-specific setup, mixed React+Vue coexistence. diff --git a/.agents/skills/nx-import/references/ESLINT.md b/.agents/skills/nx-import/references/ESLINT.md new file mode 100644 index 0000000..ea4f75f --- /dev/null +++ b/.agents/skills/nx-import/references/ESLINT.md @@ -0,0 +1,109 @@ +## ESLint + +ESLint-specific guidance for `nx import`. For generic import issues (root deps, pnpm globs, project references), see `SKILL.md`. + +--- + +### How `@nx/eslint/plugin` Works + +`@nx/eslint/plugin` scans for ESLint config files and creates a lint target for each project. It detects **both** flat config files (`eslint.config.{js,mjs,cjs,ts,mts,cts}`) and legacy config files (`.eslintrc.{json,js,cjs,mjs,yml,yaml}`). + +**Plugin options (set during `nx add @nx/eslint`):** + +```json +{ + "plugin": "@nx/eslint/plugin", + "options": { + "targetName": "eslint:lint" + } +} +``` + +**Auto-installation**: `nx import` auto-detects ESLint config files and offers to install `@nx/eslint`. Accept the offer — it registers the plugin and updates `namedInputs.production` to exclude ESLint config files. + +--- + +### Duplicate `lint` and `eslint:lint` Targets + +After import, projects will have **two** lint-related targets if the source `package.json` has a `"lint"` npm script: + +- `eslint:lint` — inferred by `@nx/eslint/plugin`; has proper caching and input/output tracking +- `lint` — created by Nx from the npm script via `nx:run-script`; no caching intelligence, just wraps `npm run lint` + +**Fix**: Remove the `"lint"` script from each project's `package.json`. Keep `"lint:fix"` if present — there is no plugin-inferred equivalent for auto-fixing. + +--- + +### Legacy `.eslintrc.*` Configs Linting Generated Files + +When `@nx/eslint/plugin` runs `eslint .` on a project with a legacy `.eslintrc.*` config that uses `parserOptions.project`, it tries to lint **all** files in the project directory including: + +- Generated `dist/**/*.d.ts` files (not in tsconfig `include`) +- The `.eslintrc.js` config file itself (not in tsconfig `include`) + +This causes `Parsing error: ESLint was configured to run on X using parserOptions.project, however that TSConfig does not include this file`. + +**Fix**: Add `ignorePatterns` to the `.eslintrc.*` config: + +```json +// .eslintrc.json +{ + "ignorePatterns": ["dist/**"] +} +``` + +```js +// .eslintrc.js — also ignore the config file itself since module.exports isn't in tsconfig +module.exports = { + ignorePatterns: ['dist/**', '.eslintrc.js'], + // ... +}; +``` + +--- + +### Flat Config `.cjs` Files Self-Linting + +When a project uses `eslint.config.cjs` (CJS flat config), `eslint .` lints the config file itself. The `require()` call on line 1 triggers `@typescript-eslint/no-require-imports`. + +**Fix**: Add the config filename to the top-level `ignores` array: + +```js +module.exports = tseslint.config( + { + ignores: ['dist/**', 'node_modules/**', 'eslint.config.cjs'], + } + // ... +); +``` + +The same applies to `eslint.config.js` in a CJS project (no `"type": "module"`) if it uses `require()`. + +--- + +### `typescript-eslint` Version Conflict With ESLint 9 + +`typescript-eslint@7.x` declares `peerDependencies: { "eslint": "^8.56.0" }`, but it is commonly used alongside `"eslint": "^9.0.0"`. npm treats this as a hard peer dep conflict and refuses to install. + +**Root cause**: `@nx/eslint` init adds `eslint@~8.57.0` at the workspace root (for its own peer deps). Workspace packages that request `eslint@^9.0.0` + `typescript-eslint@^7.0.0` trigger the conflict when npm resolves their deps. + +**Fix**: Upgrade `typescript-eslint` from `^7.0.0` to `^8.0.0` directly in the affected workspace package's `package.json`. The `tseslint.config()` API and `tseslint.configs.recommended` are identical between v7 and v8 — no config changes needed. + +```json +// packages/my-package/package.json +{ + "devDependencies": { + "typescript-eslint": "^8.0.0" + } +} +``` + +**Note**: npm's root-level `"overrides"` field does not force versions for workspace packages' direct dependencies — update each package.json individually. + +--- + +### Mixed ESLint v8 and v9 in One Workspace + +Legacy v8 and flat-config v9 packages can coexist in the same workspace. Each package resolves its own `eslint` version. The root `eslint@~8.57.0` (added by `@nx/eslint` init) is used by legacy v8 packages; v9 packages get their own hoisted `eslint@9`. + +`@nx/eslint/plugin` infers `eslint:lint` targets for **both** config formats. Legacy packages run ESLint v8 with `.eslintrc.*`; flat-config packages run ESLint v9 with `eslint.config.*`. No special nx.json configuration is needed to support both simultaneously. diff --git a/.agents/skills/nx-import/references/GRADLE.md b/.agents/skills/nx-import/references/GRADLE.md new file mode 100644 index 0000000..30dface --- /dev/null +++ b/.agents/skills/nx-import/references/GRADLE.md @@ -0,0 +1,12 @@ +## Gradle + +- If you import an entire Gradle repository into a subfolder, files like `gradlew`, `gradlew.bat`, and `gradle/wrapper` will end up inside that imported subfolder. +- The `@nx/gradle` plugin expects those files at the workspace root to infer Gradle projects/tasks automatically. +- If the target workspace has no Gradle setup yet, consider moving those files to the root (especially when using `@nx/gradle`). +- If the target workspace already has Gradle configured, avoid duplicate wrappers: remove imported duplicates from the subfolder or merge carefully. +- Because the import lands in a subfolder, Gradle project references can break; review settings and project path references, then fix any errors. +- If `@nx/gradle` is installed, run `nx show projects` to verify that Gradle projects are being inferred. + +Helpful docs: + +- https://nx.dev/docs/technologies/java/gradle/introduction diff --git a/.agents/skills/nx-import/references/JEST.md b/.agents/skills/nx-import/references/JEST.md new file mode 100644 index 0000000..64de5b7 --- /dev/null +++ b/.agents/skills/nx-import/references/JEST.md @@ -0,0 +1,228 @@ +## Jest + +Jest-specific guidance for `nx import`. For the basic "Jest Preset Missing" fix (create `jest.preset.js`, install deps), see `SKILL.md`. This file covers deeper Jest integration issues. + +--- + +### How `@nx/jest` Works + +`@nx/jest/plugin` scans for `jest.config.{ts,js,cjs,mjs,cts,mts}` and creates a `test` target for each project. + +**Plugin options:** + +```json +{ + "plugin": "@nx/jest/plugin", + "options": { + "targetName": "test" + } +} +``` + +`npx nx add @nx/jest` does two things: + +1. **Registers `@nx/jest/plugin` in `nx.json`** — without this, no `test` targets are inferred +2. Updates `namedInputs.production` to exclude test files + +**Gotcha**: `nx add @nx/jest` does NOT create `jest.preset.js` — that file is only generated when you run a generator (e.g. `@nx/jest:configuration`). For imports, you must create it manually (see "Jest Preset" section below). + +**Other gotcha**: If you create `jest.preset.js` manually but skip `npx nx add @nx/jest`, the plugin won't be registered and `nx run PROJECT:test` will fail with "Cannot find target 'test'". You need both. + +--- + +### Jest Preset + +The preset provides shared Jest configuration (test patterns, ts-jest transform, resolver, jsdom environment). + +**Root `jest.preset.js`:** + +```js +const nxPreset = require('@nx/jest/preset').default; +module.exports = { ...nxPreset }; +``` + +**Project `jest.config.ts`:** + +```ts +export default { + displayName: 'my-lib', + preset: '../../jest.preset.js', + // project-specific overrides +}; +``` + +The `preset` path is relative from the project root to the workspace root. Subdirectory imports preserve the original relative path (e.g. `../../jest.preset.js`), which resolves correctly if the import destination matches the source directory depth. + +--- + +### Testing Dependencies + +#### Core (always needed) + +``` +pnpm add -wD jest ts-jest @types/jest @nx/jest +``` + +#### Environment-specific + +- **DOM testing** (React, Vue, browser libs): `jest-environment-jsdom` +- **Node testing** (APIs, CLIs): no extra deps (Jest defaults to `node` env, but Nx preset defaults to `jsdom`) + +#### React testing + +``` +pnpm add -wD @testing-library/react @testing-library/jest-dom +``` + +#### React with Babel (non-ts-jest transform) + +Some React projects use Babel instead of ts-jest for JSX transformation: + +``` +pnpm add -wD babel-jest @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript +``` + +**When**: Project `jest.config` has `transform` using `babel-jest` instead of `ts-jest`. Common in older Nx workspaces and CRA migrations. + +#### Vue testing + +``` +pnpm add -wD @vue/test-utils +``` + +Vue projects typically use Vitest (not Jest) — see VITE.md. + +--- + +### `tsconfig.spec.json` + +Jest projects need a `tsconfig.spec.json` that includes test files: + +```json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": ["jest", "node"] + }, + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} +``` + +**Common issues after import:** + +- Missing `"types": ["jest", "node"]` — causes `describe`/`it`/`expect` to be unrecognized +- Missing `"module": "commonjs"` — Jest doesn't support ESM by default (ts-jest transpiles to CJS) +- `include` array missing test patterns — TypeScript won't check test files + +--- + +### Jest vs Vitest Coexistence + +Workspaces can have both: + +- **Jest**: Next.js apps, older React libs, Node libraries +- **Vitest**: Vite-based React/Vue apps and libs + +Both `@nx/jest/plugin` and `@nx/vite/plugin` (which infers Vitest targets) coexist without conflicts — they detect different config files (`jest.config.*` vs `vite.config.*`). + +**Target naming**: Both default to `test`. If a project somehow has both config files, rename one: + +```json +{ + "plugin": "@nx/jest/plugin", + "options": { "targetName": "jest-test" } +} +``` + +--- + +### `@testing-library/jest-dom` — Jest vs Vitest + +Projects migrating from Jest to Vitest (or workspaces with both) need different imports: + +**Jest** (in `test-setup.ts`): + +```ts +import '@testing-library/jest-dom'; +``` + +**Vitest** (in `test-setup.ts`): + +```ts +import '@testing-library/jest-dom/vitest'; +``` + +If the source used Jest but the dest workspace uses Vitest for that project type, update the import path. Also add `@testing-library/jest-dom` to tsconfig `types` array. + +--- + +### Non-Nx Source: Test Script Rewriting + +Nx rewrites `package.json` scripts during init. Test scripts get broken: + +- `"test": "jest"` → `"test": "nx test"` (circular if no executor configured) +- `"test": "vitest run"` → `"test": "nx test run"` (broken — `run` becomes an argument) + +**Fix**: Remove all rewritten test scripts. `@nx/jest/plugin` and `@nx/vite/plugin` infer test targets from config files. + +--- + +### CI Atomization + +`@nx/jest/plugin` supports splitting tests per-file for CI parallelism: + +```json +{ + "plugin": "@nx/jest/plugin", + "options": { + "targetName": "test", + "ciTargetName": "test-ci" + } +} +``` + +This creates `test-ci--src/lib/foo.spec.ts` targets for each test file, enabling Nx Cloud distribution. Not relevant during import, but useful for post-import CI setup. + +--- + +### Common Post-Import Issues + +1. **"Cannot find target 'test'"**: `@nx/jest/plugin` not registered in `nx.json`. Run `npx nx add @nx/jest` or manually add the plugin entry. + +2. **"Cannot find module 'jest-preset'"**: `jest.preset.js` missing at workspace root. Create it (see SKILL.md). + +3. **"Cannot find type definition file for 'jest'"**: Missing `@types/jest` or `tsconfig.spec.json` doesn't have `"types": ["jest", "node"]`. + +4. **Tests fail with "Cannot use import statement outside a module"**: `ts-jest` not installed or not configured as transform. Check `jest.config.ts` transform section. + +5. **Snapshot path mismatches**: After import, `__snapshots__` directories may have paths baked in. Run tests once with `--updateSnapshot` to regenerate. + +--- + +## Fix Order + +### Subdirectory Import (Nx Source) + +1. `npx nx add @nx/jest` — registers plugin in `nx.json` (does NOT create `jest.preset.js`) +2. Create `jest.preset.js` manually (see "Jest Preset" section above) +3. Install deps: `pnpm add -wD jest jest-environment-jsdom ts-jest @types/jest` +4. Install framework test deps: `@testing-library/react @testing-library/jest-dom` (React), `@vue/test-utils` (Vue) +5. Verify `tsconfig.spec.json` has `"types": ["jest", "node"]` +6. `nx run-many -t test` + +### Whole-Repo Import (Non-Nx Source) + +1. Remove rewritten test scripts from `package.json` +2. `npx nx add @nx/jest` — registers plugin (does NOT create preset) +3. Create `jest.preset.js` manually +4. Install deps (same as above) +5. Verify/fix `jest.config.*` — ensure `preset` path points to root `jest.preset.js` +6. Verify/fix `tsconfig.spec.json` — add `types`, `module`, `include` if missing +7. `nx run-many -t test` diff --git a/.agents/skills/nx-import/references/NEXT.md b/.agents/skills/nx-import/references/NEXT.md new file mode 100644 index 0000000..d9ec1f0 --- /dev/null +++ b/.agents/skills/nx-import/references/NEXT.md @@ -0,0 +1,214 @@ +## Next.js + +Next.js-specific guidance for `nx import`. For generic import issues (pnpm globs, root deps, project references, name collisions, ESLint, frontend tsconfig base settings, `@nx/react` typings, Jest preset, target name prefixing, non-Nx source handling), see `SKILL.md`. + +--- + +### `@nx/next/plugin` Inferred Targets + +`@nx/next/plugin` detects `next.config.{ts,js,cjs,mjs}` and creates these targets: + +- `build` → `next build` (with `dependsOn: ['^build']`) +- `dev` → `next dev` +- `start` → `next start` (depends on `build`) +- `serve-static` → same as `start` +- `build-deps` / `watch-deps` — for TS solution setup + +**No separate typecheck target** — Next.js runs TypeScript checking as part of `next build`. The `@nx/js/typescript` plugin provides a standalone `typecheck` target for non-Next libraries in the workspace. + +**Build target conflict**: Both `@nx/next/plugin` and `@nx/js/typescript` define a `build` target. `@nx/next/plugin` wins for Next.js projects (it detects `next.config.*`), while `@nx/js/typescript` handles libraries with `tsconfig.lib.json`. No rename needed — they coexist. + +### `withNx` in `next.config.js` + +Nx-generated Next.js projects use `composePlugins(withNx)` from `@nx/next`. This wrapper is optional for `next build` via the inferred plugin (which just runs `next build`), but it provides Nx-specific configuration. Keep it if present. + +### Root Dependencies for Next.js + +Beyond the generic root deps issue (see SKILL.md), Next.js projects typically need: + +**Core**: `react`, `react-dom`, `@types/react`, `@types/react-dom`, `@types/node`, `@nx/react` (see SKILL.md for `@nx/react` typings) +**Nx plugins**: `@nx/next` (auto-installed by import), `@nx/eslint`, `@nx/jest` +**Testing**: see SKILL.md "Jest Preset Missing" section +**ESLint**: `@next/eslint-plugin-next` (in addition to generic ESLint deps from SKILL.md) + +### Next.js Auto-Installing Dependencies via Wrong Package Manager + +Next.js detects missing `@types/react` during `next build` and tries to install it using `yarn add` regardless of the actual package manager. In a pnpm workspace, this fails with a "nearest package directory isn't part of the project" error. + +**Root cause**: `@types/react` is missing from root devDependencies. +**Fix**: Install deps at the root before building: `pnpm add -wD @types/react @types/react-dom` + +### Next.js TypeScript Config Specifics + +Next.js app tsconfigs have unique patterns compared to Vite: + +- **`noEmit: true`** with `emitDeclarationOnly: false` — Next.js handles emit, TS just checks types. This conflicts with `composite: true` from the TS solution setup. +- **`"types": ["jest", "node"]`** — includes test types in the main tsconfig (no separate `tsconfig.app.json`) +- **`"plugins": [{ "name": "next" }]`** — for IDE integration +- **`include`** references `.next/types/**/*.ts` for Next.js auto-generated types +- **`"jsx": "preserve"`** — Next.js uses its own JSX transform, not React's + +**Gotcha**: The Next.js tsconfig sets `"noEmit": true` which disables `composite` mode. This is fine because Next.js projects use `next build` for building, not `tsc`. The `@nx/js/typescript` plugin's `typecheck` target is not needed for Next.js apps. + +### `next.config.js` Lint Warning + +Imported Next.js configs may have `// eslint-disable-next-line @typescript-eslint/no-var-requires` but the project ESLint config enables different rule sets. This produces `Unused eslint-disable directive` warnings. Harmless — remove the comment or ignore. + +### `@nx/next:init` Rewrites All npm Scripts (Whole-Repo Import) + +When `@nx/next:init` runs during a whole-repo import, it rewrites the project's `package.json` scripts to prefixed `nx` calls: + +```json +{ + "dev": "nx next:dev", + "build": "nx next:build", + "start": "nx next:start" +} +``` + +This is the standard "npm Script Rewriting" issue from SKILL.md, but triggered by `@nx/next:init` rather than Nx init. **Fix**: Remove all rewritten scripts from `package.json` — `@nx/next/plugin` infers all targets from `next.config.*`. + +--- + +## Non-Nx Source (create-next-app) + +### Whole-Repo Import Recommended + +For single-project `create-next-app` repos, use whole-repo import into a subdirectory: + +```bash +nx import /path/to/source apps/web --ref=main --source=. --no-interactive +``` + +### `next-env.d.ts` + +`next build` auto-generates `next-env.d.ts` at the project root. Add `next-env.d.ts` to the dest root `.gitignore` — it is framework-generated and should not be committed. + +### ESLint: Self-Contained `eslint-config-next` + +`create-next-app` generates a flat ESLint config using `eslint-config-next` (which bundles its own plugins). This is **self-contained** — no root `eslint.config.mjs` needed, no `@nx/eslint-plugin` dependency. The `@nx/eslint/plugin` detects it and creates a lint target. + +### TypeScript: No Changes Needed + +Non-Nx Next.js projects have self-contained tsconfigs with `noEmit: true`, their own `lib`, `module`, `moduleResolution`, and `jsx` settings. Since `next build` handles type checking internally, no tsconfig modifications are needed. The project does NOT need to extend `tsconfig.base.json`. + +**Gotcha**: The `@nx/js/typescript` plugin won't create a `typecheck` target because there's no `tsconfig.lib.json`. This is fine — use `next:build` for type checking. + +### `noEmit: true` and TS Solution Setup + +Non-Nx Next.js projects use `noEmit: true`, which conflicts with Nx's TS solution setup (`composite: true`). If the dest workspace uses project references and you want the Next.js app to participate: + +1. Remove `noEmit: true`, add `composite: true`, `emitDeclarationOnly: true` +2. Add `extends: "../../tsconfig.base.json"` +3. Add `outDir` and `tsBuildInfoFile` + +**However**, this is optional for standalone Next.js apps that don't export types consumed by other workspace projects. + +### Tailwind / PostCSS + +`create-next-app` with Tailwind generates `postcss.config.mjs`. This works as-is after import — no path changes needed since PostCSS resolves relative to the project root. + +--- + +## Mixed Next.js + Vite Coexistence + +When both Next.js and Vite projects exist in the same workspace. + +### Plugin Coexistence + +Both `@nx/next/plugin` and `@nx/vite/plugin` can coexist in `nx.json`. They detect different config files (`next.config.*` vs `vite.config.*`) so there are no conflicts. The `@nx/js/typescript` plugin handles libraries. + +### Vite Standalone Project tsconfig Fixes + +Vite standalone projects (imported as whole-repo) have self-contained tsconfigs without `composite: true`. The `@nx/js/typescript` plugin's typecheck target runs `tsc --build --emitDeclarationOnly` which requires `composite`. + +**Fix**: + +1. Add `extends: "../../tsconfig.base.json"` to the root project tsconfig +2. Add `composite: true`, `declaration: true`, `declarationMap: true`, `tsBuildInfoFile` to `tsconfig.app.json` and `tsconfig.spec.json` +3. Set `moduleResolution: "bundler"` (replace `"node"`) +4. Add source files to `tsconfig.spec.json` `include` — specs import app code, and `composite` mode requires all files to be listed + +### Typecheck Target Names + +- `@nx/vite/plugin` defaults `typecheckTargetName` to `"vite:typecheck"` +- `@nx/js/typescript` uses `"typecheck"` +- Next.js projects have NO standalone typecheck target — Next.js runs type checking during `next build` + +No naming conflicts between frameworks. + +--- + +## Fix Order — Nx Source (Subdirectory Import) + +1. Import Next.js apps into `apps/` (see SKILL.md: "Application vs Library Detection") +2. Generic fixes from SKILL.md (pnpm globs, root deps, `.gitkeep` removal, frontend tsconfig base settings, `@nx/react` typings) +3. Install Next.js-specific deps: `pnpm add -wD @next/eslint-plugin-next` +4. ESLint setup (see SKILL.md: "Root ESLint Config Missing") +5. Jest setup (see SKILL.md: "Jest Preset Missing") +6. `nx reset && nx sync --yes && nx run-many -t typecheck,build,test,lint` + +## Fix Order — Non-Nx Source (create-next-app) + +1. Import into `apps/` (see SKILL.md: "Application vs Library Detection") +2. Generic fixes from SKILL.md (pnpm globs, stale files cleanup, script rewriting, target name prefixing) +3. (Optional) If app needs to export types for other workspace projects: fix `noEmit` → `composite` (see SKILL.md) +4. `nx reset && nx run-many -t next:build,eslint:lint` (or unprefixed names if renamed) + +--- + +## Iteration Log + +### Scenario 1: Basic Nx Next.js App Router + Shared Lib → TS preset (PASS) + +- Source: CNW next preset (Next.js 16, App Router) + `@nx/react:library` shared-ui +- Dest: CNW ts preset (Nx 23) +- Import: subdirectory-at-a-time (apps, libs separately) +- Errors found & fixed: + 1. pnpm-workspace.yaml: `apps`/`libs` → `apps/*`/`libs/*` + 2. Root tsconfig: `nodenext` → `bundler`, add `dom`/`dom.iterable` to `lib`, add `jsx: react-jsx` + 3. Missing `@nx/react` (for CSS module/image type defs in lib) + 4. Missing `@types/react`, `@types/react-dom`, `@types/node` + 5. Next.js trying `yarn add @types/react` — fixed by installing at root + 6. Missing `@nx/eslint`, root `eslint.config.mjs`, ESLint plugins + 7. Missing `@nx/jest`, `jest.preset.js`, `jest-environment-jsdom`, `ts-jest` +- All targets green: typecheck, build, test, lint + +### Scenario 3: Non-Nx create-next-app (App Router + Tailwind) → TS preset (PASS) + +- Source: `create-next-app@latest` (Next.js 16.1.6, App Router, Tailwind v4, flat ESLint config) +- Dest: CNW ts preset (Nx 23) +- Import: whole-repo into `apps/web` +- Errors found & fixed: + 1. pnpm-workspace.yaml: `apps/web` → `apps/*` + 2. Stale files: `node_modules/`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `.gitignore` — deleted + 3. Nx-rewritten npm scripts (`"build": "nx next:build"`, etc.) — removed +- No tsconfig changes needed — self-contained config with `noEmit: true` +- ESLint self-contained via `eslint-config-next` — no root config needed +- No test setup (create-next-app doesn't include tests) +- All targets green: next:build, eslint:lint + +### Scenario 4: Non-Nx create-next-app (alongside Vite, React Router 7, TanStack, CRA) → TS preset (PASS) + +- See VITE.md Scenario 6 for the full multi-import scenario +- Next.js-specific findings: + 1. `@nx/next:init` rewrote all scripts to `nx next:*` format — removed all rewritten scripts + 2. Stale files: `node_modules/`, `package-lock.json`, `.gitignore` — deleted (npm workspace, no pnpm files) + 3. ESLint self-contained via `eslint-config-next` — no root config needed + 4. No tsconfig changes needed — `noEmit: true` stays; `next build` handles type checking +- Targets: `next:build`, `next:dev`, `next:start`, `eslint:lint` + +### Scenario 5: Mixed Next.js (Nx) + Vite React (standalone) → TS preset (PASS) + +- Source A: CNW next preset (Next.js 16, App Router) — subdirectory import of `apps/` +- Source B: CNW react-standalone preset (Vite 7, React 19) — whole-repo import into `apps/vite-app` +- Dest: CNW ts preset (Nx 23) +- Errors found & fixed: + 1. All Scenario 1 fixes for the Next.js app + 2. Stale files from Vite source: `node_modules/`, `pnpm-lock.yaml`, `pnpm-workspace.yaml`, `.gitignore`, `nx.json` + 3. Removed rewritten scripts from Vite app's `package.json` + 4. ESLint 8 vs 9 conflict — `@nx/eslint` peer on ESLint 8 resolved wrong version. Fixed with `pnpm.overrides` + 5. Vite tsconfigs missing `composite: true`, `declaration: true` — needed for `tsc --build --emitDeclarationOnly` + 6. Vite `tsconfig.spec.json` `include` missing source files — specs import app code + 7. Vite tsconfig `moduleResolution: "node"` → `"bundler"`, added `extends: "../../tsconfig.base.json"` +- All targets green: typecheck, build, test, lint for both projects diff --git a/.agents/skills/nx-import/references/TURBOREPO.md b/.agents/skills/nx-import/references/TURBOREPO.md new file mode 100644 index 0000000..b322b54 --- /dev/null +++ b/.agents/skills/nx-import/references/TURBOREPO.md @@ -0,0 +1,62 @@ +## Turborepo + +- Nx replaces Turborepo task orchestration, but a clean migration requires handling Turborepo's config packages. +- Migration guide: https://nx.dev/docs/guides/adopting-nx/from-turborepo#easy-automated-migration-example +- Since Nx replaces Turborepo, all turbo config files and config packages become dead code and should be removed. + +## The Config-as-Package Pattern + +Turborepo monorepos ship with internal workspace packages that share configuration: + +- **`@repo/typescript-config`** (or similar) — tsconfig files (`base.json`, `nextjs.json`, `react-library.json`, etc.) +- **`@repo/eslint-config`** (or similar) — ESLint config files and all ESLint plugin dependencies + +These are not code libraries. They distribute config via Node module resolution (e.g., `"extends": "@repo/typescript-config/nextjs.json"`). This is the **default** Turborepo pattern — expect it in virtually every Turborepo import. Package names vary — check `package.json` files to identify the actual names. + +## Check for Root Config Files First + +**Before doing any config merging, check whether the destination workspace uses shared root configuration.** This decides how to handle the config packages. + +- If the workspace has a root `tsconfig.base.json` and/or root `eslint.config.mjs` that projects extend, merge the config packages into these root configs (see steps below). +- If the workspace does NOT have root config files — each project manages its own configuration independently (similar to Turborepo). In this case, **do not create root config files or merge into them**. Just remove turbo-specific parts (`turbo.json`, `eslint-plugin-turbo`) and leave the config packages in place, or ask the user how they want to handle them. + +If unclear, check for the presence of `tsconfig.base.json` at the root or ask the user. + +## Merging TypeScript Config (Only When Root tsconfig.base.json Exists) + +The config package contains a hierarchy of tsconfig files. Each project extends one via package name. + +1. **Read the config package** — trace the full inheritance chain (e.g., `nextjs.json` extends `base.json`). +2. **Update root `tsconfig.base.json`** — absorb `compilerOptions` from the base config. Add Nx `paths` for cross-project imports (Turborepo doesn't use path aliases, Nx relies on them). +3. **Update each project's `tsconfig.json`**: + - Change `"extends"` from `"@repo/typescript-config/.json"` to the relative path to root `tsconfig.base.json`. + - Inline variant-specific overrides from the intermediate config (e.g., Next.js: `"module": "ESNext"`, `"moduleResolution": "Bundler"`, `"jsx": "preserve"`, `"noEmit": true`; React library: `"jsx": "react-jsx"`). + - Preserve project-specific settings (`outDir`, `include`, `exclude`, etc.). +4. **Delete the config package** and remove it from all `devDependencies`. + +## Merging ESLint Config (Only When Root eslint.config Exists) + +The config package centralizes ESLint plugin dependencies and exports composable flat configs. + +1. **Read the config package** — identify exported configs, plugin dependencies, and inheritance. +2. **Update root `eslint.config.mjs`** — absorb base rules (JS recommended, TypeScript-ESLint, Prettier, etc.). Drop `eslint-plugin-turbo`. +3. **Update each project's `eslint.config.mjs`** — switch from importing `@repo/eslint-config/` to extending the root config, adding framework-specific plugins inline. +4. **Move ESLint plugin dependencies** from the config package to root `devDependencies`. +5. If `@nx/eslint` plugin is configured with inferred targets, remove `"lint"` scripts from project `package.json` files. +6. **Delete the config package** and remove it from all `devDependencies`. + +## General Cleanup + +- Remove turbo-specific dependencies: `turbo`, `eslint-plugin-turbo`. +- Delete all `turbo.json` files (root and per-package). +- Run workspace validation (`nx run-many -t build lint test typecheck`) to confirm nothing broke. + +## Key Pitfalls + +- **Trace the full inheritance chain** before inlining — check what each variant inherits from the base. +- **Module resolution changes** — from Node package resolution (`@repo/...`) to relative paths (`../../tsconfig.base.json`). +- **ESLint configs are JavaScript, not JSON** — handle JS imports, array spreading, and plugin objects when merging. + +Helpful docs: + +- https://nx.dev/docs/guides/adopting-nx/from-turborepo diff --git a/.agents/skills/nx-import/references/VITE.md b/.agents/skills/nx-import/references/VITE.md new file mode 100644 index 0000000..d1874bf --- /dev/null +++ b/.agents/skills/nx-import/references/VITE.md @@ -0,0 +1,397 @@ +## Vite + +Vite-specific guidance for `nx import`. For generic import issues (pnpm globs, root deps, project references, name collisions, ESLint, frontend tsconfig base settings, `@nx/react` typings, Jest preset, non-Nx source handling), see `SKILL.md`. + +--- + +### `@nx/vite/plugin` Typecheck Target + +`@nx/vite/plugin` defaults `typecheckTargetName` to `"vite:typecheck"`. If the workspace expects `"typecheck"`, set it explicitly in `nx.json`. If `@nx/js/typescript` is also registered, rename one target to avoid conflicts (e.g. `"tsc-typecheck"` for the JS plugin). + +Keep both plugins only if the workspace has non-Vite pure TS libraries — `@nx/js/typescript` handles those while `@nx/vite/plugin` handles Vite projects. + +### @nx/vite Plugin Install Failure + +Plugin init loads `vite.config.ts` before deps are available. **Fix**: `pnpm add -wD vite @vitejs/plugin-react` (or `@vitejs/plugin-vue`) first, then `pnpm exec nx add @nx/vite`. + +### Vite `resolve.alias` and `__dirname` (Non-Nx Sources) + +**`__dirname` undefined** (CJS-only): Replace with `fileURLToPath(new URL('./src', import.meta.url))` from `'node:url'`. + +**`@/` path alias**: Vite's `resolve.alias` works at runtime but TS needs matching `"paths"`. Set `"baseUrl": "."` in project tsconfig. + +**PostCSS/Tailwind**: Verify `content` globs resolve correctly after import. + +### Missing TypeScript `types` (Non-Nx Sources) + +Non-Nx tsconfigs may not declare all needed types. Ensure Vite projects include `"types": ["node", "vite/client"]` in their tsconfig. + +### `noEmit` Fix: Vite-Specific Notes + +See SKILL.md for the generic noEmit→composite fix. Vite-specific additions: + +- Non-Nx Vite projects often have **both** `tsconfig.app.json` and `tsconfig.node.json` with `noEmit` — fix both +- Solution-style tsconfigs (`"files": [], "references": [...]`) may lack `extends`. Add `extends` pointing to the dest root `tsconfig.base.json` so base settings (`moduleResolution`, `lib`) apply. +- This is safe — Vite/Vitest ignore TypeScript emit settings. + +### Dependency Version Conflicts + +**Shared Vite deps (both frameworks):** `vite`, `vitest`, `jsdom`, `@types/node`, `typescript` (dev) + +**Vite 6→7**: Typecheck fails (`Plugin` type mismatch); build/serve still works. Fix: align versions. +**Vitest 3→4**: Usually works; type conflicts may surface in shared test utils. + +--- + +## React Router 7 (Vite-Based) + +React Router 7 (`@react-router/dev`) uses Vite under the hood with a `vite.config.ts` and a `react-router.config.ts`. The `@nx/vite/plugin` detects `vite.config.ts` and creates inferred targets. + +### Targets + +`@nx/vite/plugin` creates `build`, `dev`, `serve` targets. The `build` target invokes the script defined in `package.json` (usually `react-router build`), not `vite build` directly. + +**No separate typecheck target from `@nx/vite/plugin`** — React Router 7 typegen is run as part of `typecheck` (e.g. `react-router typegen && tsc`). The `typecheck` target is inferred from the tsconfig. Keep the `typecheck` script in `package.json` if present; it is not rewritten. + +### tsconfig Notes + +React Router 7 uses a single `tsconfig.json` (no `tsconfig.app.json`/`tsconfig.node.json` split). It includes: + +- `"rootDirs": [".", "./.react-router/types"]` — for generated type files; keep as-is +- `"paths": { "~/*": ["./app/*"] }` — self-referential alias; keep as-is +- `"noEmit": true` — replace with composite settings per SKILL.md + +### Build Output + +React Router 7 outputs to `build/` (not `dist/`). Add `build` to the dest root `.gitignore`. + +### Generated Types Directory + +React Router 7 generates `.react-router/` at the project root for route type generation. Add `.react-router` to the dest root `.gitignore`. + +--- + +## TanStack Start (Vite-Based) + +TanStack Start uses Vinxi under the hood, which wraps Vite. Projects have a standard `vite.config.ts` that `@nx/vite/plugin` detects normally. + +### Targets + +`@nx/vite/plugin` creates `build`, `dev`, `preview`, `serve-static`, `typecheck` targets. The `build` target runs `vite build` which invokes the TanStack Start Vinxi pipeline (produces both client and SSR bundles). + +### tsconfig Notes + +TanStack Start uses a single `tsconfig.json` with `"allowImportingTsExtensions": true` and `"noEmit": true`. Apply the standard noEmit → composite fix. `allowImportingTsExtensions` is compatible with `emitDeclarationOnly: true` — no change needed. + +### `paths` Aliases + +TanStack Start commonly uses `"#/*": ["./src/*"]` and `"@/*": ["./src/*"]`. These are self-referential — keep as-is for a single-project app. + +### Uncommitted Source Repo + +`create-tan-stack` initializes a git repo but does NOT make an initial commit. Before importing, commit first: + +```bash +git -C /path/to/source add . && git -C /path/to/source commit -m "Initial commit" +``` + +### Generated and Build Directories + +TanStack Start / Vinxi / Nitro generate several directories that must be added to the dest root `.gitignore`: + +- `.vinxi` — Vinxi build cache +- `.tanstack` — TanStack generated files +- `.nitro` — Nitro build artifacts +- `.output` — server-side build output (SSR/edge) + +These are not covered by `dist` or `build`. + +--- + +## React-Specific + +### React Dependencies + +**Production:** `react`, `react-dom` +**Dev:** `@types/react`, `@types/react-dom`, `@vitejs/plugin-react`, `@testing-library/react`, `@testing-library/jest-dom`, `jsdom` +**ESLint (Nx sources):** `eslint-plugin-import`, `eslint-plugin-jsx-a11y`, `eslint-plugin-react`, `eslint-plugin-react-hooks` +**ESLint (`create-vite`):** `eslint-plugin-react-refresh`, `eslint-plugin-react-hooks` — self-contained flat configs can be left as-is +**Nx plugins:** `@nx/react` (generators), `@nx/vite`, `@nx/vitest`, `@nx/eslint` + +### React TypeScript Configuration + +Add `"jsx": "react-jsx"` — in `tsconfig.base.json` for single-framework workspaces, per-project for mixed (see Mixed section). + +### React ESLint Config + +```js +import nx from '@nx/eslint-plugin'; +import baseConfig from '../../eslint.config.mjs'; +export default [ + ...baseConfig, + ...nx.configs['flat/react'], + { files: ['**/*.ts', '**/*.tsx'], rules: {} }, +]; +``` + +### React Version Conflicts + +React 18 (source) + React 19 (dest): pnpm may hoist mismatched `react-dom`, causing `TypeError: Cannot read properties of undefined (reading 'S')`. **Fix**: Align versions with `pnpm.overrides`. + +### `@testing-library/jest-dom` with Vitest + +If source used Jest: change import to `@testing-library/jest-dom/vitest` in test-setup.ts, add to tsconfig `types`. + +--- + +## Vue-Specific + +### Vue Dependencies + +**Production:** `vue` (plus `vue-router`, `pinia` if used) +**Dev:** `@vitejs/plugin-vue`, `vue-tsc`, `@vue/test-utils`, `jsdom` +**ESLint:** `eslint-plugin-vue`, `vue-eslint-parser`, `@vue/eslint-config-typescript`, `@vue/eslint-config-prettier` +**Nx plugins:** `@nx/vue` (generators), `@nx/vite`, `@nx/vitest`, `@nx/eslint` (install AFTER deps — see below) + +### Vue TypeScript Configuration + +Add to `tsconfig.base.json` (single-framework) or per-project (mixed): + +```json +{ "jsx": "preserve", "jsxImportSource": "vue", "resolveJsonModule": true } +``` + +### `vue-shims.d.ts` + +Vue SFC files need a type declaration. Usually exists in each project's `src/` and imports cleanly. If missing: + +```ts +declare module '*.vue' { + import { defineComponent } from 'vue'; + const component: ReturnType; + export default component; +} +``` + +### `vue-tsc` Auto-Detection + +Both `@nx/js/typescript` and `@nx/vite/plugin` auto-detect `vue-tsc` when installed — no manual config needed. Remove source scripts like `"typecheck": "vue-tsc --noEmit"`. + +### ESLint Plugin Installation Order (Critical) + +`@nx/eslint` init **crashes** if Vue ESLint deps aren't installed first (it loads all config files). + +**Correct order:** + +1. `pnpm add -wD eslint@^9 eslint-plugin-vue vue-eslint-parser @vue/eslint-config-typescript @typescript-eslint/parser @nx/eslint-plugin typescript-eslint` +2. Create root `eslint.config.mjs` +3. Then `npx nx add @nx/eslint` + +### Vue ESLint Config Pattern + +```js +import vue from 'eslint-plugin-vue'; +import vueParser from 'vue-eslint-parser'; +import tsParser from '@typescript-eslint/parser'; +import baseConfig from '../../eslint.config.mjs'; +export default [ + ...baseConfig, + ...vue.configs['flat/recommended'], + { + files: ['**/*.vue'], + languageOptions: { parser: vueParser, parserOptions: { parser: tsParser } }, + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'], + rules: { 'vue/multi-word-component-names': 'off' }, + }, +]; +``` + +**Important**: `vue-eslint-parser` override must come **AFTER** base config — `flat/typescript` sets the TS parser globally without a `files` filter, breaking `.vue` parsing. + +`vue-eslint-parser` must be an explicit pnpm dependency (strict resolution prevents transitive import). + +**Known issue**: Some generated Vue ESLint configs omit `vue-eslint-parser`. Use the pattern above instead. + +--- + +## Mixed React + Vue + +When both frameworks coexist, several settings become per-project. + +### tsconfig `jsx` — Per-Project Only + +- React: `"jsx": "react-jsx"` in project tsconfig +- Vue: `"jsx": "preserve"`, `"jsxImportSource": "vue"` in project tsconfig +- Root: **NO** `jsx` setting + +### Typecheck — Auto-Detects Framework + +`@nx/vite/plugin` uses `vue-tsc` for Vue projects and `tsc` for React automatically. + +```json +{ + "plugins": [ + { "plugin": "@nx/eslint/plugin", "options": { "targetName": "lint" } }, + { + "plugin": "@nx/vite/plugin", + "options": { + "buildTargetName": "build", + "typecheckTargetName": "typecheck", + "testTargetName": "test" + } + } + ] +} +``` + +Remove `@nx/js/typescript` if all projects use Vite. Keep it (renamed to `"tsc-typecheck"`) only for non-Vite pure TS libs. + +### ESLint — Three-Tier Config + +1. **Root**: Base rules only, no framework-specific rules +2. **React projects**: Extend root + `nx.configs['flat/react']` +3. **Vue projects**: Extend root + `vue.configs['flat/recommended']` + `vue-eslint-parser` + +**Required packages**: Shared (`eslint@^9`, `@nx/eslint-plugin`, `typescript-eslint`, `@typescript-eslint/parser`), React (`eslint-plugin-import`, `eslint-plugin-jsx-a11y`, `eslint-plugin-react`, `eslint-plugin-react-hooks`), Vue (`eslint-plugin-vue`, `vue-eslint-parser`) + +`@nx/react`/`@nx/vue` are for generators only — no target conflicts. + +--- + +## Redundant npm Scripts After Import + +`nx import` copies `package.json` verbatim, so npm scripts come along. For Vite-based projects `@nx/vite/plugin` already infers the same targets from `vite.config.ts` — the npm scripts just shadow the plugin with weaker `nx:run-script` wrappers (no first-class caching inputs/outputs). Remove them after import. + +### Standalone Vite App (`create-vite`) + +Remove the following scripts — every one is redundant: + +| Script | Plugin replacement | +| ----------------------------- | ---------------------------------------------------------------------------- | +| `dev: vite` | `@nx/vite/plugin` → `dev` | +| `build: tsc -b && vite build` | `@nx/vite/plugin` → `build`; `typecheck` via `@nx/js/typescript` handles tsc | +| `preview: vite preview` | `@nx/vite/plugin` → `preview` | +| `lint: eslint .` | `@nx/eslint/plugin` → `eslint:lint` | + +### TanStack Start + +Remove `build`, `dev`, `preview`, and `test` scripts, but move any hardcoded `--port` flag to `vite.config.ts` first: + +```ts +// vite.config.ts +export default defineConfig({ + server: { port: 3000 }, // replaces `vite dev --port 3000` + ... +}) +``` + +### React Router 7 — Keep ALL scripts + +Do **not** remove React Router 7 scripts. They use the framework CLI (`react-router build`, `react-router dev`, `react-router-serve`) which is not interchangeable with plain `vite`: + +- `typecheck` runs `react-router typegen && tsc` — typegen must precede `tsc` or it fails on missing route types +- `start` serves the SSR bundle — no plugin equivalent + +--- + +## Fix Orders + +### Nx Source + +1. Generic fixes from SKILL.md (pnpm globs, root deps, executor paths, frontend tsconfig base settings, `@nx/react` typings) +2. Configure `@nx/vite/plugin` typecheck target +3. **React**: `jsx: "react-jsx"` (root or per-project) +4. **Vue**: `jsx: "preserve"` + `jsxImportSource: "vue"`; verify `vue-shims.d.ts`; install ESLint deps before `@nx/eslint` +5. **Mixed**: `jsx` per-project; remove/rename `@nx/js/typescript` +6. `nx sync --yes && nx reset && nx run-many -t typecheck,build,test,lint` + +### Non-Nx Source (additional steps) + +0. Import into `apps/` (see SKILL.md: "Application vs Library Detection") +1. Generic fixes from SKILL.md (stale files cleanup, pnpm globs, rewritten scripts, target name prefixing, noEmit→composite, ESLint handling) +2. Fix `noEmit` in **all** tsconfigs (app, node, etc. — non-Nx projects often have multiple) +3. Add `extends` to solution-style tsconfigs so root settings apply +4. Fix `resolve.alias` / `__dirname` / `baseUrl` +5. Ensure `types` include `vite/client` and `node` +6. Install `@nx/vite` manually if it failed during import +7. Remove redundant npm scripts so `@nx/vite/plugin` infers them natively (see "Redundant npm Scripts" section) +8. **Vue**: Add `outDir` + `**/*.vue.d.ts` to ESLint ignores +9. Full verification + +### Multiple-Source Imports + +See SKILL.md for generic multi-import (name collisions, dep refs). Vite-specific: fix tsconfig `references` paths for alternate directories (`../../libs/` → `../../libs-beta/`). + +### Non-Nx Source: React Router 7 + +1. Ensure source has at least one commit (see SKILL.md: "Source Repo Has No Commits") +2. `nx import` whole-repo into `apps/` (see SKILL.md: "Application vs Library Detection") → auto-installs `@nx/vite`, `@nx/react` +3. Stale file cleanup: `node_modules/`, `package-lock.json`, `.gitignore` +4. Fix `tsconfig.json`: `noEmit` → `composite + emitDeclarationOnly + outDir + tsBuildInfoFile` +5. Add `build` and `.react-router` to dest root `.gitignore` +6. **Keep all npm scripts** — React Router 7 uses framework CLI (`react-router build/dev`), not plain vite (see "Redundant npm Scripts" above) +7. `npm install && nx reset && nx sync --yes` + +### Non-Nx Source: TanStack Start + +1. Ensure source has at least one commit — `create-tan-stack` does NOT auto-commit (see SKILL.md) +2. `nx import` whole-repo into `apps/` (see SKILL.md: "Application vs Library Detection") → auto-installs `@nx/vite`, `@nx/vitest` +3. Stale file cleanup: `node_modules/`, `package-lock.json`, `.gitignore` +4. Fix `tsconfig.json`: `noEmit` → `composite + emitDeclarationOnly + outDir + tsBuildInfoFile` +5. Keep `allowImportingTsExtensions` — compatible with `emitDeclarationOnly: true` +6. Add `.vinxi`, `.tanstack`, `.nitro`, `.output` to dest root `.gitignore` +7. Move hardcoded `--port` from `dev` script into `vite.config.ts` (`server: { port: N }`) +8. Remove redundant npm scripts — `@nx/vite/plugin` infers `build`, `dev`, `preview`, `test` (see "Redundant npm Scripts" above) +9. `npm install && nx reset && nx sync --yes` + +### Quick Reference: React vs Vue + +| Aspect | React | Vue | +| ------------- | ------------------------ | ----------------------------------------- | +| Vite plugin | `@vitejs/plugin-react` | `@vitejs/plugin-vue` | +| Type checker | `tsc` | `vue-tsc` (auto-detected) | +| SFC support | N/A | `vue-shims.d.ts` needed | +| tsconfig jsx | `"react-jsx"` | `"preserve"` + `"jsxImportSource": "vue"` | +| ESLint parser | Standard TS | `vue-eslint-parser` + TS sub-parser | +| ESLint setup | Straightforward | Must install deps before `@nx/eslint` | +| Test utils | `@testing-library/react` | `@vue/test-utils` | + +### Quick Reference: Vite-Based React Frameworks + +| Aspect | Vite (standalone) | React Router 7 | TanStack Start | +| ------------------ | ----------------- | ----------------------- | ------------------------ | +| Build config | `vite.config.ts` | `vite.config.ts` | `vite.config.ts` | +| Build output | `dist/` | `build/` | `dist/` | +| SSR bundle | No | Yes (`build/server/`) | Yes (`dist/server/`) | +| tsconfig layout | app + node split | Single tsconfig | Single tsconfig | +| Auto-committed | Depends on tool | Usually yes | **No — commit first** | +| `nx import` plugin | `@nx/vite` | `@nx/vite`, `@nx/react` | `@nx/vite`, `@nx/vitest` | + +--- + +## Iteration Log + +### Scenario 6: Multiple non-Nx React apps (CRA, Next.js, React Router 7, TanStack Start, Vite) → TS preset (PASS) + +- Sources: 5 standalone non-Nx repos with different build tools +- Dest: CNW ts preset (Nx 22.5.1), npm workspaces, `packages/*` +- Import: whole-repo for each, sequential into `packages/` +- Pre-import fixes: + 1. Removed `packages/.gitkeep` and committed + 2. `git init && git add . && git commit` in Vite app (no git at all) + 3. `git add . && git commit` in TanStack app (git init'd but no commits) +- Import: `npm exec nx -- import packages/ --source=. --ref=main --no-interactive` + - Next.js import auto-installed `@nx/eslint`, `@nx/next` + - React Router 7 import auto-installed `@nx/vite`, `@nx/react`, `@nx/docker` (Dockerfile present) + - TanStack import auto-installed `@nx/vitest` +- Post-import fixes: + 1. Removed stale `node_modules/`, `package-lock.json`, `.gitignore` from each package + 2. Removed Nx-rewritten scripts from `board-games-nextjs/package.json` (had `"build": "nx next:build"`, etc.) + 3. Updated root `tsconfig.base.json`: `nodenext` → `bundler`, added `dom`/`dom.iterable` to lib, added `jsx: react-jsx` + 4. Added `build` to dest root `.gitignore` (CRA and React Router 7 output there) + 5. Fixed `noEmit` → `composite + emitDeclarationOnly` in: `board-games-vite/tsconfig.app.json`, `board-games-vite/tsconfig.node.json`, `board-games-react-router/tsconfig.json`, `board-games-tanstack/tsconfig.json` + 6. Fixed `tsBuildInfoFile` paths from `./node_modules/.tmp/...` to `./dist/...` + 7. Installed root `@types/react`, `@types/react-dom`, `@types/node` +- All targets green: `build` for all 5 projects; `typecheck` for Vite/React Router/TanStack; `next:build` for Next.js diff --git a/.agents/skills/nx-plugins/SKILL.md b/.agents/skills/nx-plugins/SKILL.md new file mode 100644 index 0000000..89223c7 --- /dev/null +++ b/.agents/skills/nx-plugins/SKILL.md @@ -0,0 +1,9 @@ +--- +name: nx-plugins +description: Find and add Nx plugins. USE WHEN user wants to discover available plugins, install a new plugin, or add support for a specific framework or technology to the workspace. +--- + +## Finding and Installing new plugins + +- List plugins: `pnpm nx list` +- Install plugins `pnpm nx add `. Example: `pnpm nx add @nx/react`. diff --git a/.agents/skills/nx-run-tasks/SKILL.md b/.agents/skills/nx-run-tasks/SKILL.md new file mode 100644 index 0000000..7f1263a --- /dev/null +++ b/.agents/skills/nx-run-tasks/SKILL.md @@ -0,0 +1,58 @@ +--- +name: nx-run-tasks +description: Helps with running tasks in an Nx workspace. USE WHEN the user wants to execute build, test, lint, serve, or run any other tasks defined in the workspace. +--- + +You can run tasks with Nx in the following way. + +Keep in mind that you might have to prefix things with npx/pnpx/yarn if the user doesn't have nx installed globally. Look at the package.json or lockfile to determine which package manager is in use. + +For more details on any command, run it with `--help` (e.g. `nx run-many --help`, `nx affected --help`). + +## Understand which tasks can be run + +You can check those via `nx show project --json`, for example `nx show project myapp --json`. It contains a `targets` section which has information about targets that can be run. You can also just look at the `package.json` scripts or `project.json` targets, but you might miss out on inferred tasks by Nx plugins. + +## Run a single task + +``` +nx run : +``` + +where `project` is the project name defined in `package.json` or `project.json` (if present). + +## Run multiple tasks + +``` +nx run-many -t build test lint typecheck +``` + +You can pass a `-p` flag to filter to specific projects, otherwise it runs on all projects. You can also use `--exclude` to exclude projects, and `--parallel` to control the number of parallel processes (default is 3). + +Examples: + +- `nx run-many -t test -p proj1 proj2` — test specific projects +- `nx run-many -t test --projects=*-app --exclude=excluded-app` — test projects matching a pattern +- `nx run-many -t test --projects=tag:api-*` — test projects by tag + +## Run tasks for affected projects + +Use `nx affected` to only run tasks on projects that have been changed and projects that depend on changed projects. This is especially useful in CI and for large workspaces. + +``` +nx affected -t build test lint +``` + +By default it compares against the base branch. You can customize this: + +- `nx affected -t test --base=main --head=HEAD` — compare against a specific base and head +- `nx affected -t test --files=libs/mylib/src/index.ts` — specify changed files directly + +## Useful flags + +These flags work with `run`, `run-many`, and `affected`: + +- `--skipNxCache` — rerun tasks even when results are cached +- `--verbose` — print additional information such as stack traces +- `--nxBail` — stop execution after the first failed task +- `--configuration=` — use a specific configuration (e.g. `production`) diff --git a/.agents/skills/nx-workspace/SKILL.md b/.agents/skills/nx-workspace/SKILL.md new file mode 100644 index 0000000..4b5110a --- /dev/null +++ b/.agents/skills/nx-workspace/SKILL.md @@ -0,0 +1,286 @@ +--- +name: nx-workspace +description: "Explore and understand Nx workspaces. USE WHEN answering questions about the workspace, projects, or tasks. ALSO USE WHEN an nx command fails or you need to check available targets/configuration before running a task. EXAMPLES: 'What projects are in this workspace?', 'How is project X configured?', 'What depends on library Y?', 'What targets can I run?', 'Cannot find configuration for task', 'debug nx task failure'." +--- + +# Nx Workspace Exploration + +This skill provides read-only exploration of Nx workspaces. Use it to understand workspace structure, project configuration, available targets, and dependencies. + +Keep in mind that you might have to prefix commands with `npx`/`pnpx`/`yarn` if nx isn't installed globally. Check the lockfile to determine the package manager in use. + +## Listing Projects + +Use `nx show projects` to list projects in the workspace. + +The project filtering syntax (`-p`/`--projects`) works across many Nx commands including `nx run-many`, `nx release`, `nx show projects`, and more. Filters support explicit names, glob patterns, tag references (e.g. `tag:name`), directories, and negation (e.g. `!project-name`). + +```bash +# List all projects +nx show projects + +# Filter by pattern (glob) +nx show projects --projects "apps/*" +nx show projects --projects "shared-*" + +# Filter by tag +nx show projects --projects "tag:publishable" +nx show projects -p 'tag:publishable,!tag:internal' + +# Filter by target (projects that have a specific target) +nx show projects --withTarget build + +# Combine filters +nx show projects --type lib --withTarget test +nx show projects --affected --exclude="*-e2e" +nx show projects -p "tag:scope:client,packages/*" + +# Negate patterns +nx show projects -p '!tag:private' +nx show projects -p '!*-e2e' + +# Output as JSON +nx show projects --json +``` + +## Project Configuration + +Use `nx show project --json` to get the full resolved configuration for a project. + +**Important**: Do NOT read `project.json` directly - it only contains partial configuration. The `nx show project --json` command returns the full resolved config including inferred targets from plugins. + +You can read the full project schema at `node_modules/nx/schemas/project-schema.json` to understand nx project configuration options. + +```bash +# Get full project configuration +nx show project my-app --json + +# Extract specific parts from the JSON +nx show project my-app --json | jq '.targets' +nx show project my-app --json | jq '.targets.build' +nx show project my-app --json | jq '.targets | keys' + +# Check project metadata +nx show project my-app --json | jq '{name, root, sourceRoot, projectType, tags}' +``` + +## Target Information + +Targets define what tasks can be run on a project. + +```bash +# List all targets for a project +nx show project my-app --json | jq '.targets | keys' + +# Get full target configuration +nx show project my-app --json | jq '.targets.build' + +# Check target executor/command +nx show project my-app --json | jq '.targets.build.executor' +nx show project my-app --json | jq '.targets.build.command' + +# View target options +nx show project my-app --json | jq '.targets.build.options' + +# Check target inputs/outputs (for caching) +nx show project my-app --json | jq '.targets.build.inputs' +nx show project my-app --json | jq '.targets.build.outputs' + +# Find projects with a specific target +nx show projects --withTarget serve +nx show projects --withTarget e2e +``` + +## Workspace Configuration + +Read `nx.json` directly for workspace-level configuration. +You can read the full project schema at `node_modules/nx/schemas/nx-schema.json` to understand nx project configuration options. + +```bash +# Read the full nx.json +cat nx.json + +# Or use jq for specific sections +cat nx.json | jq '.targetDefaults' +cat nx.json | jq '.namedInputs' +cat nx.json | jq '.plugins' +cat nx.json | jq '.generators' +``` + +Key nx.json sections: + +- `targetDefaults` - Default configuration applied to all targets of a given name +- `namedInputs` - Reusable input definitions for caching +- `plugins` - Nx plugins and their configuration +- ...and much more, read the schema or nx.json for details + +## Affected Projects + +If the user is asking about affected projects, read the [affected projects reference](references/AFFECTED.md) for detailed commands and examples. + +## Common Exploration Patterns + +### "What's in this workspace?" + +```bash +nx show projects +nx show projects --type app +nx show projects --type lib +``` + +### "How do I build/test/lint project X?" + +```bash +nx show project X --json | jq '.targets | keys' +nx show project X --json | jq '.targets.build' +``` + +### "What depends on library Y?" + +```bash +# Use the project graph to find dependents +nx graph --print | jq '.graph.dependencies | to_entries[] | select(.value[].target == "Y") | .key' +``` + +## Programmatic Answers + +When processing nx CLI results, use command-line tools to compute the answer programmatically rather than counting or parsing output manually. Always use `--json` flags to get structured output that can be processed with `jq`, `grep`, or other tools you have installed locally. + +### Listing Projects + +```bash +nx show projects --json +``` + +Example output: + +```json +["my-app", "my-app-e2e", "shared-ui", "shared-utils", "api"] +``` + +Common operations: + +```bash +# Count projects +nx show projects --json | jq 'length' + +# Filter by pattern +nx show projects --json | jq '.[] | select(startswith("shared-"))' + +# Get affected projects as array +nx show projects --affected --json | jq '.' +``` + +### Project Details + +```bash +nx show project my-app --json +``` + +Example output: + +```json +{ + "root": "apps/my-app", + "name": "my-app", + "sourceRoot": "apps/my-app/src", + "projectType": "application", + "tags": ["type:app", "scope:client"], + "targets": { + "build": { + "executor": "@nx/vite:build", + "options": { "outputPath": "dist/apps/my-app" } + }, + "serve": { + "executor": "@nx/vite:dev-server", + "options": { "buildTarget": "my-app:build" } + }, + "test": { + "executor": "@nx/vite:test", + "options": {} + } + }, + "implicitDependencies": [] +} +``` + +Common operations: + +```bash +# Get target names +nx show project my-app --json | jq '.targets | keys' + +# Get specific target config +nx show project my-app --json | jq '.targets.build' + +# Get tags +nx show project my-app --json | jq '.tags' + +# Get project root +nx show project my-app --json | jq -r '.root' +``` + +### Project Graph + +```bash +nx graph --print +``` + +Example output: + +```json +{ + "graph": { + "nodes": { + "my-app": { + "name": "my-app", + "type": "app", + "data": { "root": "apps/my-app", "tags": ["type:app"] } + }, + "shared-ui": { + "name": "shared-ui", + "type": "lib", + "data": { "root": "libs/shared-ui", "tags": ["type:ui"] } + } + }, + "dependencies": { + "my-app": [ + { "source": "my-app", "target": "shared-ui", "type": "static" } + ], + "shared-ui": [] + } + } +} +``` + +Common operations: + +```bash +# Get all project names from graph +nx graph --print | jq '.graph.nodes | keys' + +# Find dependencies of a project +nx graph --print | jq '.graph.dependencies["my-app"]' + +# Find projects that depend on a library +nx graph --print | jq '.graph.dependencies | to_entries[] | select(.value[].target == "shared-ui") | .key' +``` + +## Troubleshooting + +### "Cannot find configuration for task X:target" + +```bash +# Check what targets exist on the project +nx show project X --json | jq '.targets | keys' + +# Check if any projects have that target +nx show projects --withTarget target +``` + +### "The workspace is out of sync" + +```bash +nx sync +nx reset # if sync doesn't fix stale cache +``` diff --git a/.agents/skills/nx-workspace/references/AFFECTED.md b/.agents/skills/nx-workspace/references/AFFECTED.md new file mode 100644 index 0000000..e30f18f --- /dev/null +++ b/.agents/skills/nx-workspace/references/AFFECTED.md @@ -0,0 +1,27 @@ +## Affected Projects + +Find projects affected by changes in the current branch. + +```bash +# Affected since base branch (auto-detected) +nx show projects --affected + +# Affected with explicit base +nx show projects --affected --base=main +nx show projects --affected --base=origin/main + +# Affected between two commits +nx show projects --affected --base=abc123 --head=def456 + +# Affected apps only +nx show projects --affected --type app + +# Affected excluding e2e projects +nx show projects --affected --exclude="*-e2e" + +# Affected by uncommitted changes +nx show projects --affected --uncommitted + +# Affected by untracked files +nx show projects --affected --untracked +``` diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..887af87 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,13 @@ +{ + "extraKnownMarketplaces": { + "nx-claude-plugins": { + "source": { + "source": "github", + "repo": "nrwl/nx-ai-agents-config" + } + } + }, + "enabledPlugins": { + "nx@nx-claude-plugins": true + } +} diff --git a/.codex/agents/ci-monitor-subagent.toml b/.codex/agents/ci-monitor-subagent.toml new file mode 100644 index 0000000..591c7cb --- /dev/null +++ b/.codex/agents/ci-monitor-subagent.toml @@ -0,0 +1,46 @@ +developer_instructions = """ +# CI Monitor Subagent + +You are a CI helper. You call ONE MCP tool per invocation and return the result. Do not loop, poll, or sleep. + +## Commands + +The main agent tells you which command to run: + +### FETCH_STATUS + +Call `ci_information` with the provided branch and select fields. Return a JSON object with ONLY these fields: +`{ cipeStatus, selfHealingStatus, verificationStatus, selfHealingEnabled, selfHealingSkippedReason, failureClassification, failedTaskIds, verifiedTaskIds, couldAutoApplyTasks, autoApplySkipped, autoApplySkipReason, userAction, cipeUrl, commitSha, shortLink }` + +### FETCH_HEAVY + +Call `ci_information` with heavy select fields. Summarize the heavy content and return: + +```json +{ + "shortLink": "...", + "failedTaskIds": ["..."], + "verifiedTaskIds": ["..."], + "suggestedFixDescription": "...", + "suggestedFixSummary": "...", + "selfHealingSkipMessage": "...", + "taskFailureSummaries": [{ "taskId": "...", "summary": "..." }] +} +``` + +Do NOT return raw suggestedFix diffs or raw taskOutputSummary — summarize them. +The main agent uses these summaries to understand what failed and attempt local fixes. + +### UPDATE_FIX + +Call `update_self_healing_fix` with the provided shortLink and action (APPLY/REJECT/RERUN_ENVIRONMENT_STATE). Return the result message (success/failure string). + +### FETCH_THROTTLE_INFO + +Call `ci_information` with the provided URL. Return ONLY: `{ shortLink, cipeUrl }` + +## Important + +- Execute ONE command and return immediately +- Do NOT poll, loop, sleep, or make decisions +- Extract and return ONLY the fields specified for each command — do NOT dump the full MCP response""" diff --git a/.codex/config.toml b/.codex/config.toml index 1021665..5e0026a 100644 --- a/.codex/config.toml +++ b/.codex/config.toml @@ -1,8 +1,17 @@ -approval_policy = "on-request" -sandbox_mode = "workspace-write" + +approval_policy = 'on-request' +sandbox_mode = 'workspace-write' +mcp_servers.nx-mcp.command = 'npx' +mcp_servers.nx-mcp.args = [ + 'nx', + 'mcp', +] +features.multi_agent = true [sandbox_workspace_write] network_access = true -# Nx's startLocalRegistry helper shells out to `npm config set/delete ... --ws=false`, -# which temporarily writes the local registry auth token to the user npm config at ~/.npmrc. -writable_roots = ["~/.npmrc"] +writable_roots = ['~/.npmrc'] + +[agents.ci-monitor-subagent] +description = 'CI helper for /monitor-ci. Fetches CI status, retrieves fix details, or updates self-healing fixes. Executes one MCP tool call and returns the result.' +config_file = 'agents/ci-monitor-subagent.toml' diff --git a/.gitignore b/.gitignore index ce078f8..4ae49bc 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,5 @@ Thumbs.db .nx/cache .nx/workspace-data .claude/worktrees -.claude/settings.local.json \ No newline at end of file +.claude/settings.local.json +.nx/polygraph diff --git a/AGENTS.md b/AGENTS.md index 7a80cef..e6feb07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,7 @@ # AGENTS.md ## Task Completion Requirements + - Always review your work and verify the results of your work using the appropriate tools before considering tasks completed. ## Project Snapshot @@ -17,8 +18,32 @@ If a tradeoff is required, choose correctness and robustness over short-term con ## Maintainability -Long-term maintainability is a core priority. If you add new functionality, first check if there is shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem. If you are not sure, research to find a good solution. +Long-term maintainability is a core priority. If you add new functionality, first check if there is shared logic that can be extracted to a separate module. Duplicate logic across multiple files is a code smell and should be avoided. Don't be afraid to change existing code. Don't take shortcuts by just adding local logic to solve a problem. If you are not sure, research to find a good solution. ## Technology Stack - Use npm + + + + +## General Guidelines for working with Nx + +- For navigating/exploring the workspace, invoke the `nx-workspace` skill first - it has patterns for querying projects, targets, and dependencies +- When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through `nx` (i.e. `nx run`, `nx run-many`, `nx affected`) instead of using the underlying tooling directly +- Prefix nx commands with the workspace's package manager (e.g., `pnpm nx build`, `npm exec nx test`) - avoids using globally installed CLI +- You have access to the Nx MCP server and its tools, use them to help the user +- For Nx plugin best practices, check `node_modules/@nx//PLUGIN.md`. Not all plugins have this file - proceed without it if unavailable. +- NEVER guess CLI flags - always check nx_docs or `--help` first when unsure + +## Scaffolding & Generators + +- For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the `nx-generate` skill FIRST before exploring or calling MCP tools + +## When to use nx_docs + +- USE for: advanced config options, unfamiliar flags, migration guides, plugin configuration, edge cases +- DON'T USE for: basic generator syntax (`nx g @nx/react:app`), standard commands, things you already know +- The `nx-generate` skill handles generator discovery internally - don't call nx_docs just to look up generator syntax + + diff --git a/e2e/nx-forge-e2e/src/application.generator.spec.ts b/e2e/nx-forge-e2e/src/application.generator.spec.ts index c818c87..97212d5 100644 --- a/e2e/nx-forge-e2e/src/application.generator.spec.ts +++ b/e2e/nx-forge-e2e/src/application.generator.spec.ts @@ -1,40 +1,137 @@ -import { existsSync, readFileSync } from 'node:fs'; +import { + existsSync, + readFileSync, + readdirSync, + statSync, + writeFileSync, +} from 'node:fs'; import { join } from 'node:path'; import { generateForgeApp } from './utils/generate-forge-app'; import { runNxCommandAsync } from './utils/async-commands'; -import { cleanupTestProject, createTestProject } from './utils/test-project'; +import { + cleanupTestWorkspace, + createTestWorkspace, +} from './utils/test-workspace'; + +const describeDirectoryTree = (directory: string, depth = 0): string => { + if (!existsSync(directory)) { + return '(missing)'; + } + + const prefix = ' '.repeat(depth); + const entries = readdirSync(directory).sort(); + + return entries + .map((entry) => { + const entryPath = join(directory, entry); + const isDirectory = statSync(entryPath).isDirectory(); + const label = `${prefix}${entry}${isDirectory ? '/' : ''}`; + + if (!isDirectory) { + return label; + } + + const childTree = describeDirectoryTree(entryPath, depth + 1); + return childTree === '' ? label : `${label}\n${childTree}`; + }) + .join('\n'); +}; + +const expectWebpackBuildOutput = async ( + workspaceDirectory: string, + appName: string +) => { + const { stdout } = await runNxCommandAsync(`run ${appName}:build`, { + cwd: workspaceDirectory, + }); + const outputDir = join(workspaceDirectory, 'dist', 'apps', appName); + const indexJsPath = join(outputDir, 'src', 'index.js'); + + if (!existsSync(indexJsPath)) { + throw new Error( + [ + `Expected generated build output at ${indexJsPath}.`, + `Build stdout:\n${stdout}`, + `Output tree for ${outputDir}:\n${describeDirectoryTree(outputDir)}`, + ].join('\n\n') + ); + } + + expect(existsSync(join(outputDir, 'src', 'main.js'))).toBe(false); + + await runNxCommandAsync(`run ${appName}:package`, { + cwd: workspaceDirectory, + }); + expect(existsSync(join(outputDir, 'manifest.yml'))).toBe(true); + expect(existsSync(join(outputDir, 'package.json'))).toBe(true); +}; + +const configureWebpackTaskInference = ( + workspaceDirectory: string, + enabled: boolean +) => { + const nxJsonPath = join(workspaceDirectory, 'nx.json'); + const nxJson = JSON.parse(readFileSync(nxJsonPath, 'utf8')); + const plugins = (nxJson.plugins ?? []).filter((plugin) => + typeof plugin === 'string' + ? plugin !== '@nx/webpack/plugin' + : plugin.plugin !== '@nx/webpack/plugin' + ); + nxJson.useInferencePlugins = enabled; + + if (enabled) { + plugins.push('@nx/webpack/plugin'); + } + + nxJson.plugins = plugins; + writeFileSync(nxJsonPath, JSON.stringify(nxJson, null, 2) + '\n', 'utf8'); +}; + +const enableWebpackTaskInference = async (workspaceDirectory: string) => { + configureWebpackTaskInference(workspaceDirectory, true); + await runNxCommandAsync( + 'generate @nx/webpack:init --addPlugin=true --interactive=false', + { + cwd: workspaceDirectory, + } + ); +}; + +const disableWebpackTaskInference = (workspaceDirectory: string) => { + configureWebpackTaskInference(workspaceDirectory, false); +}; describe('Forge application generator', () => { - let projectDirectory: string; + let workspaceDirectory: string; beforeAll(() => { - projectDirectory = createTestProject(); + workspaceDirectory = createTestWorkspace(); }); afterAll(async () => { try { - if (projectDirectory) { - await runNxCommandAsync('reset', { cwd: projectDirectory }); + if (workspaceDirectory) { + await runNxCommandAsync('reset', { cwd: workspaceDirectory }); } } finally { - cleanupTestProject(projectDirectory); + cleanupTestWorkspace(workspaceDirectory); } }); it('should generate a Forge app', async () => { const appName = await generateForgeApp({ - cwd: projectDirectory, + cwd: workspaceDirectory, directory: 'apps', options: '--bundler=webpack', }); expect( - existsSync(join(projectDirectory, 'apps', appName, 'manifest.yml')) + existsSync(join(workspaceDirectory, 'apps', appName, 'manifest.yml')) ).toBe(true); expect( - existsSync(join(projectDirectory, 'apps', appName, 'webpack.config.js')) + existsSync(join(workspaceDirectory, 'apps', appName, 'webpack.config.js')) ).toBe(true); expect( - existsSync(join(projectDirectory, 'apps', appName, 'src', 'index.ts')) + existsSync(join(workspaceDirectory, 'apps', appName, 'src', 'index.ts')) ).toBe(true); }); @@ -42,19 +139,21 @@ describe('Forge application generator', () => { it('should generate a Forge app in the specified directory', async () => { const subdir = 'subdir'; const appName = await generateForgeApp({ - cwd: projectDirectory, + cwd: workspaceDirectory, directory: subdir, options: `--bundler=webpack`, }); expect( - existsSync(join(projectDirectory, subdir, appName, 'manifest.yml')) + existsSync(join(workspaceDirectory, subdir, appName, 'manifest.yml')) ).toBe(true); expect( - existsSync(join(projectDirectory, subdir, appName, 'webpack.config.js')) + existsSync( + join(workspaceDirectory, subdir, appName, 'webpack.config.js') + ) ).toBe(true); expect( - existsSync(join(projectDirectory, subdir, appName, 'src', 'index.ts')) + existsSync(join(workspaceDirectory, subdir, appName, 'src', 'index.ts')) ).toBe(true); }); }); @@ -62,17 +161,67 @@ describe('Forge application generator', () => { describe('--tags', () => { it('should generate a Forge app with tags added to the project', async () => { const appName = await generateForgeApp({ - cwd: projectDirectory, + cwd: workspaceDirectory, directory: 'apps', options: `--tags e2etag,e2ePackage`, }); const project = JSON.parse( readFileSync( - join(projectDirectory, 'apps', appName, 'project.json'), + join(workspaceDirectory, 'apps', appName, 'project.json'), 'utf8' ) ); expect(project.tags).toEqual(['e2etag', 'e2ePackage']); }); }); + + it('should create expected output with inferred webpack plugin', async () => { + await enableWebpackTaskInference(workspaceDirectory); + + const appName = await generateForgeApp({ + cwd: workspaceDirectory, + directory: 'apps', + options: '--bundler=webpack', + }); + expect( + readFileSync( + join(workspaceDirectory, 'apps', appName, 'webpack.config.js'), + 'utf8' + ) + ).toContain('NxAppWebpackPlugin'); + const project = JSON.parse( + readFileSync( + join(workspaceDirectory, 'apps', appName, 'project.json'), + 'utf8' + ) + ); + expect(project.targets?.build).toBeUndefined(); + + await expectWebpackBuildOutput(workspaceDirectory, appName); + }); + + it('should create expected output with legacy webpack executor', async () => { + disableWebpackTaskInference(workspaceDirectory); + + const appName = await generateForgeApp({ + cwd: workspaceDirectory, + directory: 'apps', + options: '--bundler=webpack', + }); + expect( + readFileSync( + join(workspaceDirectory, 'apps', appName, 'webpack.config.js'), + 'utf8' + ) + ).toContain('composePlugins'); + const project = JSON.parse( + readFileSync( + join(workspaceDirectory, 'apps', appName, 'project.json'), + 'utf8' + ) + ); + expect(project.targets?.build).toBeDefined(); + + await expectWebpackBuildOutput(workspaceDirectory, appName); + }); }); diff --git a/e2e/nx-forge-e2e/src/basic-setup.spec.ts b/e2e/nx-forge-e2e/src/basic-setup.spec.ts index 26e4960..005cca5 100644 --- a/e2e/nx-forge-e2e/src/basic-setup.spec.ts +++ b/e2e/nx-forge-e2e/src/basic-setup.spec.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { GraphQLClient } from 'graphql-request'; import { generateForgeApp } from './utils/generate-forge-app'; +import { cleanupRegisteredForgeApp } from './utils/cleanup-registered-forge-app'; import { Credentials, ForgeInstallationContext, @@ -9,28 +10,28 @@ import { getDeveloperSpaceId, getForgeInstallationContext, } from './utils/config'; -import { createClient, deleteApp } from './utils/atlassian-graphql-client'; +import { createClient } from './utils/atlassian-graphql-client'; import { runCommandAsync, runForgeCommandAsync, runNxCommandAsync, } from './utils/async-commands'; import { - cleanupTestProject, - createTestProject, -} from './utils/test-project'; + cleanupTestWorkspace, + createTestWorkspace, +} from './utils/test-workspace'; import stripAnsi = require('strip-ansi'); describe('Forge lifecycle', () => { // initialize before all tests - let projectDirectory: string; + let workspaceDirectory: string; let developerCredentials: Credentials; let apiClient: GraphQLClient; let installationContext: ForgeInstallationContext; let developerSpaceId: string; beforeAll(async () => { - projectDirectory = createTestProject(); + workspaceDirectory = createTestWorkspace(); developerCredentials = getCredentials(); apiClient = createClient(developerCredentials); installationContext = getForgeInstallationContext(); @@ -38,31 +39,31 @@ describe('Forge lifecycle', () => { // Initialize the Forge CLI, otherwise commands may fail due to expected interactive input await runCommandAsync(`npx forge settings set usage-analytics false`, { - cwd: projectDirectory, + cwd: workspaceDirectory, silenceError: true, }); }); afterAll(async () => { try { - if (projectDirectory) { - await runNxCommandAsync('reset', { cwd: projectDirectory }); + if (workspaceDirectory) { + await runNxCommandAsync('reset', { cwd: workspaceDirectory }); } } finally { - cleanupTestProject(projectDirectory); + cleanupTestWorkspace(workspaceDirectory); } }); it('should generate, build, package, register, deploy and install a Forge app', async () => { const appName = await generateForgeApp({ - cwd: projectDirectory, + cwd: workspaceDirectory, directory: 'apps', }); // Build const nxBuildResult = await runNxCommandAsync(`run ${appName}:build`, { - cwd: projectDirectory, + cwd: workspaceDirectory, silenceError: true, }); expect(nxBuildResult.stderr).toEqual(''); @@ -72,9 +73,12 @@ describe('Forge lifecycle', () => { // Package - const nxPackageResult = await runNxCommandAsync(`run ${appName}:package`, { - cwd: projectDirectory, - }); + const nxPackageResult = await runNxCommandAsync( + `run ${appName}:package`, + { + cwd: workspaceDirectory, + } + ); expect(nxPackageResult.stderr).toEqual(''); expect(stripAnsi(nxPackageResult.stdout)).toEqual( expect.stringContaining('Successfully ran target package for project') @@ -83,7 +87,7 @@ describe('Forge lifecycle', () => { // Register const unregisteredOutputManifestContent = readFileSync( - join(projectDirectory, 'dist', 'apps', appName, 'manifest.yml'), + join(workspaceDirectory, 'dist', 'apps', appName, 'manifest.yml'), 'utf8' ); expect(unregisteredOutputManifestContent).toContain( @@ -93,7 +97,7 @@ describe('Forge lifecycle', () => { const nxRegisterResult = await runNxCommandAsync( `run ${appName}:register --accept-terms --developer-space-id ${developerSpaceId}`, { - cwd: projectDirectory, + cwd: workspaceDirectory, silenceError: true, } ); @@ -107,7 +111,7 @@ describe('Forge lifecycle', () => { /ari:cloud:ecosystem::app\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/; const registeredOutputManifestContent = readFileSync( - join(projectDirectory, 'dist', 'apps', appName, 'manifest.yml'), + join(workspaceDirectory, 'dist', 'apps', appName, 'manifest.yml'), 'utf8' ); const [registeredAppId] = @@ -117,7 +121,7 @@ describe('Forge lifecycle', () => { expect(registeredAppId).not.toEqual(''); const projectManifestContent = readFileSync( - join(projectDirectory, 'apps', appName, 'manifest.yml'), + join(workspaceDirectory, 'apps', appName, 'manifest.yml'), 'utf8' ); expect(projectManifestContent).toContain(registeredAppId); @@ -129,7 +133,7 @@ describe('Forge lifecycle', () => { const nxDeployResult = await runNxCommandAsync( `run ${appName}:deploy --no-verify`, { - cwd: projectDirectory, + cwd: workspaceDirectory, silenceError: true, } ); @@ -141,7 +145,7 @@ describe('Forge lifecycle', () => { const installResult = await runForgeCommandAsync( `install --product=${installationContext.product} --site=${installationContext.siteUrl} --environment ${installationContext.environment} --non-interactive`, { - cwd: join(projectDirectory, 'dist', 'apps', appName), + cwd: join(workspaceDirectory, 'dist', 'apps', appName), silenceError: true, } ); @@ -149,34 +153,12 @@ describe('Forge lifecycle', () => { expect(stripAnsi(installResult.stdout)).toMatch(/Install.*complete/); } finally { if (registeredAppId) { - try { - await runForgeCommandAsync( - `uninstall --product=${installationContext.product} --site=${installationContext.siteUrl} --environment ${installationContext.environment} --non-interactive`, - { - cwd: join(projectDirectory, 'dist', 'apps', appName), - silenceError: true, - } - ); - } catch (error) { - console.warn( - `Failed to uninstall Forge app ${registeredAppId}`, - error - ); - } - - try { - const result = await deleteApp(registeredAppId)(apiClient); - if (!result.success) { - console.warn( - `Failed to delete registered app ${registeredAppId}: ${result.errors}` - ); - } - } catch (error) { - console.warn( - `Failed to delete registered app ${registeredAppId}`, - error - ); - } + await cleanupRegisteredForgeApp({ + appDirectory: join(workspaceDirectory, 'dist', 'apps', appName), + appId: registeredAppId, + apiClient, + installationContext, + }); } } }); diff --git a/e2e/nx-forge-e2e/src/utils/async-commands.ts b/e2e/nx-forge-e2e/src/utils/async-commands.ts index 2827c91..a6b35da 100644 --- a/e2e/nx-forge-e2e/src/utils/async-commands.ts +++ b/e2e/nx-forge-e2e/src/utils/async-commands.ts @@ -9,6 +9,11 @@ const getCommandEnv = (env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv => { // warnings on stderr unless NO_COLOR is removed for the child command. delete commandEnv.NO_COLOR; + // The e2e suite creates and mutates fresh workspaces on disk between + // commands. Disabling the Nx daemon avoids stale project graph state + // causing follow-up commands to miss newly generated projects. + commandEnv.NX_DAEMON = 'false'; + return commandEnv; }; @@ -36,11 +41,22 @@ export const runCommandAsync = ( { cwd: opts.cwd, env: getCommandEnv(opts.env), + maxBuffer: 10 * 1024 * 1024, windowsHide: true, }, (err, stdout, stderr) => { if (!opts.silenceError && err) { - reject(err); + const error = new Error( + [ + `Command failed: ${command}`, + stdout && `stdout:\n${stdout}`, + stderr && `stderr:\n${stderr}`, + ] + .filter(Boolean) + .join('\n\n') + ); + reject(error); + return; } resolve({ stdout, stderr }); } diff --git a/e2e/nx-forge-e2e/src/utils/atlassian-graphql-client.ts b/e2e/nx-forge-e2e/src/utils/atlassian-graphql-client.ts index e721ede..399283b 100644 --- a/e2e/nx-forge-e2e/src/utils/atlassian-graphql-client.ts +++ b/e2e/nx-forge-e2e/src/utils/atlassian-graphql-client.ts @@ -1,7 +1,7 @@ import { gql, GraphQLClient } from 'graphql-request'; import { Credentials } from './config'; -interface DeleteAppResponse { +export interface DeleteAppResponse { success: boolean; errors?: [{ message: string }]; } diff --git a/e2e/nx-forge-e2e/src/utils/cleanup-registered-forge-app.ts b/e2e/nx-forge-e2e/src/utils/cleanup-registered-forge-app.ts new file mode 100644 index 0000000..0ac1307 --- /dev/null +++ b/e2e/nx-forge-e2e/src/utils/cleanup-registered-forge-app.ts @@ -0,0 +1,84 @@ +import { GraphQLClient } from 'graphql-request'; +import { deleteApp, DeleteAppResponse } from './atlassian-graphql-client'; +import { runForgeCommandAsync } from './async-commands'; +import { ForgeInstallationContext } from './config'; + +const DELETE_RETRY_INTERVAL_MS = 2_000; +const DELETE_RETRY_TIMEOUT_MS = 10_000; + +const sleep = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +const formatDeleteErrors = (errors?: DeleteAppResponse['errors']) => + errors?.map((error) => error.message).join('; ') ?? 'Unknown error'; + +const hasInstallationsError = (errors?: DeleteAppResponse['errors']) => + errors?.some((error) => + error.message.toLowerCase().includes('has installations') + ) ?? false; + +/** + * Best-effort cleanup for a Forge app registered during e2e tests. + * + * The Forge GraphQL delete mutation can briefly lag behind the CLI uninstall + * and report that the app still has installations. To keep the suite stable, + * we uninstall first and then retry deletion for a short, bounded window + * before giving up with a warning. + */ +export const cleanupRegisteredForgeApp = async ({ + appDirectory, + appId, + apiClient, + installationContext, +}: { + appDirectory: string; + appId: string; + apiClient: GraphQLClient; + installationContext: ForgeInstallationContext; +}) => { + try { + await runForgeCommandAsync( + `uninstall --product=${installationContext.product} --site=${installationContext.siteUrl} --environment ${installationContext.environment} --non-interactive`, + { + cwd: appDirectory, + silenceError: true, + } + ); + } catch (error) { + console.warn(`Failed to uninstall Forge app ${appId}`, error); + } + + const maxAttempts = + Math.floor(DELETE_RETRY_TIMEOUT_MS / DELETE_RETRY_INTERVAL_MS) + 1; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const result = await deleteApp(appId)(apiClient); + + if (result.success) { + return; + } + + const errorMessage = formatDeleteErrors(result.errors); + + // Atlassian can report lingering installations for a few seconds after uninstall. + if (attempt < maxAttempts && hasInstallationsError(result.errors)) { + await sleep(DELETE_RETRY_INTERVAL_MS); + continue; + } + + console.warn(`Failed to delete registered app ${appId}: ${errorMessage}`); + return; + } catch (error) { + if (attempt < maxAttempts) { + await sleep(DELETE_RETRY_INTERVAL_MS); + continue; + } + + console.warn(`Failed to delete registered app ${appId}`, error); + return; + } + } +}; diff --git a/e2e/nx-forge-e2e/src/utils/test-project.ts b/e2e/nx-forge-e2e/src/utils/test-project.ts deleted file mode 100644 index 793d619..0000000 --- a/e2e/nx-forge-e2e/src/utils/test-project.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { execSync } from 'node:child_process'; -import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; - -const TEST_PROJECTS_ROOT = join(process.cwd(), 'tmp'); - -const runCommand = (command: string, cwd: string) => { - execSync(command, { - cwd, - stdio: 'inherit', - env: process.env, - windowsHide: true, - }); -}; - -const uniqueProjectName = () => - `test-project-${Date.now()}-${Math.floor(Math.random() * 100000)}`; - -export const createTestProject = (projectName = uniqueProjectName()): string => { - const projectDirectory = join(TEST_PROJECTS_ROOT, projectName); - - rmSync(projectDirectory, { - recursive: true, - force: true, - }); - mkdirSync(dirname(projectDirectory), { - recursive: true, - }); - - runCommand( - `npx -y create-nx-workspace@latest ${projectName} --preset=apps --nxCloud=skip --packageManager=npm --no-interactive`, - TEST_PROJECTS_ROOT - ); - - const nxJsonPath = join(projectDirectory, 'nx.json'); - const nxJson = JSON.parse(readFileSync(nxJsonPath, 'utf8')); - nxJson.analytics = false; - writeFileSync(nxJsonPath, JSON.stringify(nxJson, null, 2) + '\n', 'utf8'); - - runCommand( - 'npx nx add @toolsplus/nx-forge@e2e --interactive=false', - projectDirectory - ); - - return projectDirectory; -}; - -export const cleanupTestProject = (projectDirectory?: string) => { - if (!projectDirectory) { - return; - } - - rmSync(projectDirectory, { - recursive: true, - force: true, - }); -}; diff --git a/e2e/nx-forge-e2e/src/utils/test-workspace.ts b/e2e/nx-forge-e2e/src/utils/test-workspace.ts new file mode 100644 index 0000000..81d7bd5 --- /dev/null +++ b/e2e/nx-forge-e2e/src/utils/test-workspace.ts @@ -0,0 +1,60 @@ +import { execSync } from 'node:child_process'; +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { workspaceRoot } from '@nx/devkit'; + +const TEST_WORKSPACES_ROOT = join(workspaceRoot, 'tmp'); + +const runCommand = (command: string, cwd: string) => { + execSync(command, { + cwd, + stdio: 'inherit', + env: process.env, + windowsHide: true, + }); +}; + +const uniqueWorkspaceName = () => + `test-workspace-${Date.now()}-${Math.floor(Math.random() * 100000)}`; + +export const createTestWorkspace = ( + workspaceName = uniqueWorkspaceName() +): string => { + const workspaceDirectory = join(TEST_WORKSPACES_ROOT, workspaceName); + + rmSync(workspaceDirectory, { + recursive: true, + force: true, + }); + mkdirSync(dirname(workspaceDirectory), { + recursive: true, + }); + + runCommand( + `npx -y create-nx-workspace@latest ${workspaceName} --preset=apps --nxCloud=skip --packageManager=npm --no-interactive`, + TEST_WORKSPACES_ROOT + ); + + const nxJsonPath = join(workspaceDirectory, 'nx.json'); + const nxJson = JSON.parse(readFileSync(nxJsonPath, 'utf8')); + nxJson.analytics = false; + writeFileSync(nxJsonPath, JSON.stringify(nxJson, null, 2) + '\n', 'utf8'); + + runCommand( + 'npx nx add @toolsplus/nx-forge@e2e --interactive=false', + workspaceDirectory + ); + + return workspaceDirectory; +}; + +export const cleanupTestWorkspace = (workspaceDirectory?: string) => { + if (!workspaceDirectory) { + return; + } + + rmSync(workspaceDirectory, { + recursive: true, + force: true, + }); +}; diff --git a/packages/nx-forge/src/generators/application/files/webpack.config.js__tmpl__ b/packages/nx-forge/src/generators/application/files/webpack.config.js__tmpl__ index 84d17f3..5f636a1 100644 --- a/packages/nx-forge/src/generators/application/files/webpack.config.js__tmpl__ +++ b/packages/nx-forge/src/generators/application/files/webpack.config.js__tmpl__ @@ -1,18 +1,15 @@ <% if (webpackPluginOptions) { %> const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin'); -const { join } = require('path'); module.exports = { output: { - path: join(__dirname, '<%= offset %><%= webpackPluginOptions.outputPath %>'), - library: { - type: 'commonjs2', - }, + filename: 'index.js', }, plugins: [ new NxAppWebpackPlugin({ target: 'node', compiler: 'tsc', + outputPath: '<%= offset %><%= webpackPluginOptions.outputPath %>', main: '<%= webpackPluginOptions.main %>', outputFileName: 'index.js', tsConfig: '<%= webpackPluginOptions.tsConfig %>', @@ -28,7 +25,7 @@ module.exports = { ], }; <% } else { %> -const { composePlugins, withNx} = require('@nx/webpack'); +const { composePlugins, withNx } = require('@nx/webpack'); // Nx plugins for webpack. module.exports = composePlugins( diff --git a/tools/scripts/stop-local-registry.ts b/tools/scripts/stop-local-registry.ts index 31d5d34..2bdd11b 100644 --- a/tools/scripts/stop-local-registry.ts +++ b/tools/scripts/stop-local-registry.ts @@ -3,8 +3,8 @@ * It is meant to be called in jest's globalTeardown. */ -export default () => { +export default async () => { if (global.stopLocalRegistry) { - global.stopLocalRegistry(); + await global.stopLocalRegistry(); } };