Skip to content

Fix Node restore SDK anchor and introduce extensible Node toolchain - #104

Open
TomProkop wants to merge 20 commits into
masterfrom
fix/noderestore-sdk-resolver-anchor
Open

Fix Node restore SDK anchor and introduce extensible Node toolchain#104
TomProkop wants to merge 20 commits into
masterfrom
fix/noderestore-sdk-resolver-anchor

Conversation

@TomProkop

@TomProkop TomProkop commented Jul 31, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #90. Fixes #93.

Bare dotnet restore can now hydrate Node dependencies even though the Tasks package's build-transitive imports are excluded during NuGet restore. The SDK provides the restore anchor and re-evaluates cold-cache projects after NuGet downloads the Tasks package.

This also replaces the transitional single-adapter design with an extensible Node toolchain boundary that models package managers and orchestrators as independent roles.

Architecture

  • NodeToolchain detects and resolves one package manager (npm, pnpm, yarn, or bun) and an optional orchestrator (rush) independently.
  • ResolveNodeToolchain performs Node-specific priority selection, duplicate/tie validation, explicit override handling, and preserves candidate metadata.
  • NodeToolchain/* owns detection and provider-specific eligibility, including Rush project/topology evaluation.
  • NodeRestore/* owns dependency hydration only.
  • NodeBuild/* owns build execution only.
  • Rush owns restore/build only when the project is registered in rush.json; unregistered projects retain a non-owning Rush selection and fall back to their selected package manager.
  • External NuGet packages register public NodePackageManagerCandidate or NodeOrchestratorCandidate items and hook the public NodeRestore/NodeBuild lifecycles with standard MSBuild target hooks. The selected items are exposed as NodeSelectedPackageManager and NodeSelectedOrchestrator, with provider metadata preserved. Built-in providers use the same contract.
  • Public NodeBuildArgument items carry project-type arguments to every built-in or external build provider; Rush validates its forwarded arguments from optional RushParameterName metadata.

The file and target names now map directly to their modules, for example:

Targets/NodeToolchain/Rush.targets  -> _NodeToolchainRushDetect
Targets/NodeRestore/Rush.targets    -> _NodeRestoreRushResolve / _NodeRestoreRushRun
Targets/NodeBuild/Rush.targets      -> _NodeBuildRush

Restore and Rush behavior

  • Supports npm, pnpm, Yarn Classic/Berry, and Bun (bun.lock and bun.lockb).
  • Uses frozen/reproducible install commands in CI when a lockfile is present.
  • Preserves scoped Rush restore and Rush subspace topology.
  • Serializes Rush operations with one named mutex per Rush root and retains bounded retry for external Rush lock contention.
  • Keeps the expanded Rush incremental gate and bootstrap-corruption diagnostics.
  • NodeRestoreCommand remains an exact command override and suppresses built-in restore providers.

Compatibility and breaking changes

  • NodeRootPath is the Node project-root property across all Node project types.
  • Removed NodeRestoreProjectDirectory.
  • Removed NodePackageManager=rush; Rush is configured through NodeOrchestrator.
  • Removed the transitional adapter contract (SelectToolAdapter, adapter candidates/phases, and adapter terminology).
  • Removed old private target aliases rather than retaining forwarding or dead compatibility code.
  • CI environment detection now exists only in CIDetection.props; GenerateGitVersion consumes the resolved IsRunningInCI value.

Validation

  • Tasks project builds successfully.
  • Targeted MSBuild fixtures cover automatic and explicit npm/pnpm/Yarn/Bun selection, Yarn Classic/Berry, both Bun lock formats, None roles, custom restore commands, public build arguments, Rush+pnpm independent roles, registered Rush subspaces, unregistered non-owning Rush fallback, and external package-manager/orchestrator providers consuming only the public contract.
  • The Tasks NuGet package contains the final NodeToolchain, NodeRestore, and NodeBuild files and no replaced adapter files.
  • Removed architecture symbols have zero repository references.

TomProkop and others added 4 commits August 3, 2026 12:05
…ary/CodeApp

A standalone "dotnet restore" sets ExcludeRestorePackageImports=true, so NuGet
never imports ordinary PackageReference/buildTransitive-delivered .targets
files - including the Tasks package's NodeRestore.targets - during that
operation. The per-project-type NodeRestore anchors (_PcfNodeRestore,
_ScriptLibraryNodeRestore, _CodeAppNodeRestore) lived in exactly those excluded
files, so a bare restore silently skipped Node/rush hydration entirely.

Moves the anchor into Sdk.targets, which is imported through the MSBuild
SDK-resolver mechanism and is therefore not subject to
ExcludeRestorePackageImports. Scoped to Pcf/ScriptLibrary/CodeApp only, since
every other project type also references this SDK for unrelated shared targets
(e.g. GenerateVersionNumber) but must never attempt Node/rush restore.

Two additional issues surfaced during real restore testing against a live PCF
control, both fixed here:
- $(NuGetPackageRoot) is not reliably populated in the nested MSBuild
  evaluation NuGet uses internally to build the restore graph, silently
  preventing the anchor from ever firing. Sdk.props now derives the NuGet
  package cache root from its own resolved file location instead.
- On a brand new machine/cache, the Tasks package isn't downloaded yet when
  the anchor's nested evaluation runs, so Node deps were never hydrated by
  that restore at all. A second hook, AfterTargets="Restore", re-invokes the
  anchor in a freshly re-evaluated nested build once the outer restore has
  finished downloading packages.

Validated end-to-end against FileExplorer (a real, complex PCF control) with
disposable local test packages: a single cold-cache "dotnet restore" now
hydrates node_modules via rush, a warm-cache restore is idempotent, a normal
dotnet build has no duplicate-import warnings, and a Solution-type project
restore correctly triggers zero Node/rush activity for itself.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sdk.targets previously mixed four unrelated concerns in one file:
Microsoft.NET.Sdk passthrough, per-project-type PackageReference wiring,
Git-versioning defaults, and the NodeRestore restore-time anchor. Split
into:
- Sdk.targets: thin entry point, imports the three files below.
- Sdk.PackageReference.targets: wires in the per-project-type Dataverse
  package based on $(ProjectType).
- Sdk.GitVersioning.targets: Git-based version-number defaults +
  ResolveGitBranch.
- Sdk.NodeRestore.targets: the restore-time Node dependency hydration
  anchor, with a top-of-file note clarifying it's restore-time only -
  build-time Node delegation lives in each project type's own .targets
  file and the Tasks package's NodeBuild targets.

Pure reorganization, no logic changes. Validated: dotnet build 0
warnings/0 errors; cold-cache restore against a disposable FileExplorer
test project hydrates node_modules via rush; warm restore is idempotent
(~2s, no-op); full dotnet build succeeds with 0 warnings (no MSB4011
duplicate-import regression); Solution-type negative test confirms
NodeRestore never fires for non-Node project types.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Follow-up to the Sdk.targets split - the one-line comments in
Pcf/ScriptLibrary/CodeApp's own .targets files pointed at Sdk.targets,
which no longer directly contains _NodeRestoreAnchor after the split.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@TomProkop
TomProkop force-pushed the fix/noderestore-sdk-resolver-anchor branch 3 times, most recently from 07e1a91 to 4a76129 Compare August 3, 2026 10:39
@TomProkop TomProkop changed the title Fix: bare dotnet restore doesn't hydrate Node deps for Pcf/ScriptLibrary/CodeApp Fix Node restore anchor + split Sdk.targets + rename TypeScriptDir to NodeRootPath + scope Rush restore Aug 3, 2026
@TomProkop
TomProkop force-pushed the fix/noderestore-sdk-resolver-anchor branch from 4a76129 to d5fecf5 Compare August 3, 2026 11:05
…ectDirectory, scope Rush restore

- NodeRootPath defined in shared ProjectPaths.props (applies to all Node-based
  project types: Pcf, ScriptLibrary, CodeApp)
- Follows SolutionRootPath pattern: relative path (default .), resolved to
  NodeRootFullPath via GetFullPath
- NodeRestoreProjectDirectory removed — NodeRootFullPath used directly throughout
  the NodeRestore system (no redundant indirection)
- Backward compat: TypeScriptDir still accepted if NodeRootPath is not set
- Rush restore scoped conditionally: --to . for project-level restores,
  unscoped for solution-level restores (detected via SolutionPath)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@TomProkop
TomProkop force-pushed the fix/noderestore-sdk-resolver-anchor branch from d5fecf5 to 987002a Compare August 3, 2026 13:13
@TomProkop
TomProkop requested a review from zekelinAlex August 3, 2026 14:20
MSBuild sets $(SolutionPath) to the literal string "*Undefined*" when not
building from a solution, rather than leaving it empty. The previous condition
only checked for empty, so project-level restores never got the --to . scope.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

There’s a confirmed backward-compat risk where NodeRootFullPath is computed in a way that can break absolute-path overrides carried over from TypeScriptDir, plus a couple of documentation inaccuracies introduced by the refactor.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR fixes restore-time Node dependency hydration for Node-based Dataverse project types by anchoring NodeRestore from the SDK-resolved targets (so it runs during a bare dotnet restore), while also refactoring the SDK targets layout and standardizing the Node project root property name.

Changes:

  • Add Sdk.NodeRestore.targets with a restore-time _NodeRestoreAnchor (plus cold-cache follow-up) and split Sdk.targets into focused imports.
  • Rename TypeScriptDirNodeRootPath (with compatibility mapping) and update PCF/ScriptLibrary/CodeApp to use $(NodeRootFullPath).
  • Scope Rush restore (rush update/install) based on solution-level vs project-level invocation.
File summaries
File Description
src/Sdk/Sdk/Sdk.targets Split into thin entry point importing per-concern target files.
src/Sdk/Sdk/Sdk.props Derive _TALXISDevKitNuGetPackageRoot from resolved SDK path for restore-time imports.
src/Sdk/Sdk/Sdk.PackageReference.targets New file: centralizes per-ProjectType Dataverse package reference wiring.
src/Sdk/Sdk/Sdk.GitVersioning.targets New file: isolates Git versioning defaults and ResolveGitBranch.
src/Sdk/Sdk/Sdk.NodeRestore.targets New file: anchors NodeRestore during restore using SDK-resolved imports, with cold-cache handling.
src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Rush.targets Use NodeRootFullPath and add Rush restore scoping (--to .) when not building from a solution.
src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Generic.targets Switch package.json path resolution to NodeRootFullPath.
src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Detection.targets Detection now walks upward from NodeRootFullPath instead of NodeRestoreProjectDirectory.
src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/CustomCommand.targets Run custom restore commands from NodeRootFullPath.
src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore.targets Update docs/comments to reflect NodeRootPath/NodeRootFullPath model.
src/Dataverse/Tasks/msbuild/tasks/Props/ProjectPaths.props Introduce NodeRootPath + legacy TypeScriptDir mapping and compute NodeRootFullPath.
src/Dataverse/Tasks/msbuild/buildTransitive/TALXIS.DevKit.Build.Dataverse.Tasks.targets Mark Tasks import as already-loaded to avoid duplicate import warnings.
src/Dataverse/ScriptLibrary/README.md Document NodeRootPath in place of TypeScriptDir.
src/Dataverse/ScriptLibrary/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.ScriptLibrary.targets Update ScriptLibrary Node paths to NodeRootFullPath and rely on SDK restore anchor.
src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets Remove per-type restore anchor; use NodeRootFullPath for Node build directory.
src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets Remove per-type restore anchor; use NodeRootFullPath consistently for Node paths.
docs/NodeDependencies.md Replace TypeScriptDir mention with NodeRootPath/NodeRootFullPath documentation.
docs/BuildProcess.md Update ScriptLibrary narrative to reference NodeRootFullPath (but hook list needs alignment).
Review details
  • Files reviewed: 18/18 changed files
  • Comments generated: 3
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread docs/BuildProcess.md Outdated
Comment thread src/Dataverse/Tasks/msbuild/tasks/Props/ProjectPaths.props Outdated
Comment thread docs/NodeDependencies.md Outdated
TomProkop and others added 5 commits August 3, 2026 17:22
- Use MSBuild::NormalizePath instead of GetFullPath for NodeRootFullPath
  so absolute-path overrides (from TypeScriptDir or explicit NodeRootPath)
  are handled correctly.
- Remove stale _ScriptLibraryNodeRestore hook from BuildProcess.md (now
  handled by _NodeRestoreAnchor in Sdk.NodeRestore.targets).
- Fix NodeDependencies.md detection description to reflect all project
  types walk from NodeRootFullPath (not just ScriptLibrary).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@zekelinAlex zekelinAlex left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took this for a proper spin today (INT0015, the TALXIS repo and a pd-package sandbox). The idea is right, but I hit four issues while testing. All of them are fixed on the branch now:

  • restore died for a ScriptLibrary/CodeApp without package.json: the per-type RunNodeBuild defaults aren't imported during a bare restore, so the anchor ran npm in a folder with nothing to install. The anchor now probes for package.json itself (98cc00e).
  • fresh clone + solution-level restore couldn't hydrate Node deps: NuGet runs no per-project Restore target at solution scope, so the AfterTargets="Restore" fallback never fires there. NodeRestore now also sits in each type's build chain, so the next build (even with --no-restore) self-heals; the remaining one-restore gap is documented (2ad04b8).
  • the rush restore scoping didn't actually work: "rush update" has no --to in any rush version, and "install --to ." ran from the repo root where "." can't resolve. Scoping now engages only on a never-installed workspace, runs "install --to ." from the project directory and falls back to a full update outside CI (03f76dd).
  • the big one: a solution-scope restore fires the anchor for every project in parallel, and rush's pre-lock phases (pnpm bootstrap in ~/.rush, lockfile copies in common/temp) corrupt each other under concurrency. On a 124-project rush repo that meant 137 errors and a trashed pnpm cache. Rush invocations are now serialized behind a per-workspace mutex, an up-to-date gate keeps warm restores at zero rush spawns, and a stale last-install.flag gets cleared when a project's node_modules were deleted (03f76dd).

Verified end to end: INT0015 (fresh hydration, warm restore ~15-30s with no rush processes, delete node_modules then restore self-heals), a full TALXIS publish over the legacy TypeScriptDir layout, and a pd-package sandbox rebuilt from git clean. Docs updated to match (cf63ef9). CodeApp and the npm ci path weren't covered by this pass.

TomProkop and others added 3 commits August 5, 2026 23:33
- bound mutex names with a stable hash and reduce retry duration
- parse Rush project/subspace configuration with a typed MSBuild task
- support project dependency selection across standard Rush subspaces
- preserve the warm gate while tracking shared Rush inputs
- rely on Rush to self-heal project links without deleting its state

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rename SelectNodeAdapter → SelectToolAdapter (ecosystem-agnostic)
- Rename Detection.targets → Selection.targets, _NodeRestoreResolve → _NodeRestoreSelect
- Split Generic.targets → Npm.targets, Pnpm.targets, Yarn.targets, Bun.targets
- Rename Rush targets to tool-first convention: _NodeRestoreRushDetect/Resolve/Run
- Rename build targets: _NodeBuildDirect, _NodeBuildDelegateToRush
- Rename retry targets: _NodeExecWithRetry, _NodeExecRetry*, _NodeExecRushLockMessage
- Switch NodeBuild.targets from wildcard to explicit imports
- Add docs/MSBuildConventions.md documenting naming rules and adapter pattern

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@TomProkop TomProkop changed the title Fix Node restore anchor + split Sdk.targets + rename TypeScriptDir to NodeRootPath + scope Rush restore Fix Node restore SDK anchor, add pluggable adapters, align naming convention Aug 7, 2026
- Extract CI detection into Props/CIDetection.props (single source of truth)
- Remove NodeRestoreAdapterConfigureDependsOn and RunDependsOn extension points
- Remove _NodeRestoreAdapterConfigurePhase and RunPhase aggregator targets
- Tool resolve/run targets use AfterTargets="_NodeRestoreSelect" (standard MSBuild)
- Built-in and external NuGet adapters now follow the same pattern
- Fix Bun lockfile detection: support both bun.lock (v1.2+) and bun.lockb (legacy)
- Fix Yarn Berry CI: just 'yarn install' (Berry auto-detects CI, no --immutable needed)
- NodeRestore entry point simplifies to DependsOnTargets="_NodeRestoreSelect"

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@zekelinAlex
zekelinAlex self-requested a review August 10, 2026 10:23
Separate package-manager and orchestrator detection from restore and build lifecycles, remove the transitional adapter architecture, and centralize CI detection.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@TomProkop TomProkop changed the title Fix Node restore SDK anchor, add pluggable adapters, align naming convention Fix Node restore SDK anchor and introduce extensible Node toolchain Aug 10, 2026
TomProkop and others added 4 commits August 10, 2026 16:24
Promote candidate and selected items to public MSBuild names, add the public NodeToolchain target, and keep built-in providers from claiming third-party package managers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep existing ScriptLibrary projects on TypeScriptDir without migration and document its unchanged normalized path semantics across normal and cold-cache restore evaluation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document NodeRootPath as the sole configuration surface while retaining a time-bounded compatibility bridge in implementation comments.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Split package-manager build providers, expose structured build arguments, and move Rush lifecycle eligibility into detection so every provider consumes the same selected-item contract.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants