Skip to content

feat: extract @nadle/project package for project discovery and workspace scanning #538

@nam-hle

Description

@nam-hle

Context

Follow-up to #533 (@nadle/kernel) and #536 (tracking issue). The @nadle/kernel package (Layer 1) provides pure resolution primitives. This issue covers Layer 2: extracting project discovery and workspace scanning from packages/nadle into a reusable @nadle/project package.

Currently, only packages/nadle (the CLI) can discover workspaces and build a project model. The language server operates in isolation per-document with zero project awareness — it can't resolve workspace-qualified references (shared:build), can't validate cross-file dependencies, and can't offer completions from other workspace config files.

What to extract from nadle core

Logic Current location What it does
Root detection core/options/project-resolver.ts:76-121 Find monorepo root via nadle.root: true in package.json, fallback to lock file detection (@manypkg/find-root)
Workspace discovery core/options/project-resolver.ts:76-121 Read pnpm-workspace.yaml / package.json workspaces field via @manypkg/tools to enumerate all workspaces
Config file location core/options/project-resolver.ts:46-74 For each workspace, scan for nadle.config.{js,ts,mjs,mts} (first match wins)
Workspace metadata core/models/project/workspace.ts, root-workspace.ts Build { id, label, relativePath, absolutePath, configFilePath, packageJson } per workspace using kernel's deriveWorkspaceId()
Dependency resolution core/models/project/dependency-resolver/*.ts Scan package.json for workspace:* deps (pnpm/yarn) or matching versions (npm) to build workspace dependency graph
Alias resolution core/models/project/alias-resolver.ts Map workspace paths to human-readable labels via config
Validation core/models/project/project.ts:88-111 Validate workspace labels (non-empty, no duplicates, no ID conflicts)

Package design

packages/project/
  src/
    index.ts              # Public API exports
    project-discovery.ts  # findProjectRoot(), discoverWorkspaces()
    config-locator.ts     # locateConfigFiles()
    dependency-resolver.ts # resolveWorkspaceDependencies()
    types.ts              # ProjectInfo, WorkspaceInfo interfaces
  package.json            # deps: @nadle/kernel, @manypkg/find-root, @manypkg/tools, find-up
  tsconfig.build.json

Public API

// Types
interface WorkspaceInfo {
  id: string;
  label: string;
  relativePath: string;
  absolutePath: string;
  dependencies: string[];
  packageName: string;
  packageVersion: string | undefined;
  configFilePath: string | null;
}

interface ProjectInfo {
  rootDir: string;
  packageManager: "pnpm" | "npm" | "yarn";
  rootConfigFilePath: string | null;
  workspaces: WorkspaceInfo[];
}

// Discovery
function findProjectRoot(startDir: string): Promise<string>;
function discoverWorkspaces(rootDir: string): Promise<ProjectInfo>;
function locateConfigFiles(project: ProjectInfo): Promise<ProjectInfo>;
function resolveWorkspaceDependencies(project: ProjectInfo): ProjectInfo;

Dependencies

  • @nadle/kernel — workspace ID derivation, constants
  • @manypkg/find-root — fallback monorepo root detection
  • @manypkg/tools — workspace enumeration (pnpm, npm, yarn)
  • find-up — upward directory search

Consumer changes

Language server (packages/language-server)

The biggest beneficiary. With project awareness, the LSP can:

  1. On initialization: discover the project root and all workspaces
  2. Eagerly scan all workspace nadle.config.* files (not just open documents)
  3. Cross-file go-to-definition: resolve dependsOn: ["shared:build"] to the task registration in packages/shared/nadle.config.ts
  4. Cross-file completions: when typing in a dependsOn string, suggest tasks from all workspaces
  5. Workspace-qualified reference validation: verify that shared:build actually exists in the shared workspace
  6. Hover info enrichment: show which workspace a referenced task belongs to

nadle core (packages/nadle)

Refactor ProjectResolver and Project to delegate to @nadle/project for discovery, keeping only nadle-specific concerns (config loading via jiti, AsyncLocalStorage binding, task registration).

IntelliJ plugin (separate repo)

Can consume @nadle/project via a Kotlin/JVM port or a Node.js bridge process.

Acceptance criteria

  • @nadle/project package with the API above
  • nadle core refactored to use @nadle/project for discovery
  • Language server uses @nadle/project for workspace-aware features
  • Unit tests for project discovery, workspace enumeration, dependency resolution
  • Integration test: discover workspaces in packages/sample-app

Part of #536

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions