Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
678effd
Fix: bare dotnet restore doesn't hydrate Node deps for Pcf/ScriptLibr…
TomProkop Jul 31, 2026
5b1b1cc
Split Sdk.targets by concern for readability
TomProkop Jul 31, 2026
a96138b
Update breadcrumb comments to reference Sdk.NodeRestore.targets
TomProkop Jul 31, 2026
a69ed81
Strip trailing whitespace in Sdk.props
TomProkop Jul 31, 2026
987002a
feat: rename TypeScriptDir to NodeRootPath, eliminate NodeRestoreProj…
TomProkop Aug 3, 2026
18c7be9
Fix Rush scoping: handle MSBuild *Undefined* sentinel for SolutionPath
TomProkop Aug 3, 2026
e7fbb62
Address review: NormalizePath for absolute overrides, fix docs
TomProkop Aug 3, 2026
98cc00e
fix: skip restore-time NodeRestore anchor when package.json is absent
zekelinAlex Aug 3, 2026
2ad04b8
fix: hydrate Node deps at build time as solution-restore safety net
zekelinAlex Aug 3, 2026
03f76dd
fix: serialize Rush via workspace mutex, gate and self-heal restores
zekelinAlex Aug 3, 2026
cf63ef9
docs: describe Rush serialization, up-to-date gate and scoping rules
zekelinAlex Aug 3, 2026
937f343
Harden Rush coordination and resolve subspace topology
TomProkop Aug 5, 2026
d15325b
Add pluggable Node restore adapters
TomProkop Aug 7, 2026
e72012f
Refactor: align naming convention and split per-tool adapters
TomProkop Aug 7, 2026
90d212d
Simplify adapter pipeline and de-duplicate CI detection
TomProkop Aug 7, 2026
1443e7a
Refactor Node toolchain role resolution
TomProkop Aug 10, 2026
eea68c5
Expose public Node toolchain extension contract
TomProkop Aug 10, 2026
4f7f042
Clarify TypeScriptDir compatibility contract
TomProkop Aug 10, 2026
737893b
Hide TypeScriptDir compatibility from docs
TomProkop Aug 10, 2026
54d1c45
Unify built-in and external Node providers
TomProkop Aug 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Generates version numbers based on Git commit history, applying these versions a
Project dependency folders are analyzed for Git changes to be reflected in generated version numbers. See [here](/docs/Versioning.md) for more details.

#### Node Dependency Restore
Pcf/ScriptLibrary/CodeApp projects auto-detect and run the right Node package manager (npm, pnpm, Yarn, Bun, or Rush) instead of a hardcoded `npm install`. See [here](/docs/NodeDependencies.md) for more details.
Pcf/ScriptLibrary/CodeApp projects auto-detect the Node package manager (npm, pnpm, Yarn, or Bun) and optional Rush orchestrator instead of a hardcoded `npm install`. See [here](/docs/NodeDependencies.md) for more details.

## Packages

Expand Down
15 changes: 7 additions & 8 deletions docs/BuildProcess.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,8 @@ Like the Plugin package, it replaces ILRepack's default auto-hook with a no-op t
Main hooks:

- imports `Microsoft.PowerApps.VisualStudio.Pcf.props` / `.targets`
- `_PcfNodeRestore` runs `AfterTargets="CollectPackageReferences"` (not `BeforeTargets="BeforeBuild"` - this is what makes a bare `dotnet restore` at the repo/solution root hydrate Node deps too, see [NodeDependencies.md](NodeDependencies.md#verb-parity)) and calls the shared `NodeRestore` target
- `PcfBuild` is overridden (Rush-resolved projects only) to delegate the actual build to Rush's own `build` command instead of Microsoft's own `npm run build` `<Exec>`, forwarding the build mode as a `--build-mode` Rush custom command-line parameter (reusing Microsoft's own `$(PcfBuildMode)` Debug/Release mapping) - see [NodeDependencies.md](NodeDependencies.md#pcf-specific-forwarding-the-build-mode-as---build-mode)
- the SDK-level `_NodeRestoreAnchor` runs after `CollectPackageReferences`, so a bare repository/solution `dotnet restore` hydrates Node dependencies; `PcfBuild` also depends on `NodeRestore` as a cold-cache safety net
- `PcfBuild` is overridden to invoke the shared `NodeBuild` target; Rush-owned projects delegate to Rush, otherwise the selected package manager runs the build script, forwarding the build mode as a `--build-mode` Rush custom command-line parameter (reusing Microsoft's own `$(PcfBuildMode)` Debug/Release mapping) - see [NodeDependencies.md](NodeDependencies.md#pcf-specific-forwarding-the-build-mode-as---build-mode)
- `_ApplyPcfVersionAfterBuild` runs `AfterTargets="PcfBuild"` (after `ControlManifest.xml` actually exists) and applies Git-based versioning
- `_EnsurePcfStubAssembly` runs before `Publish` / `GetCopyToPublishDirectoryItems` and creates a stub DLL if needed
- `PcfCopyToPublish` runs `AfterTargets="Publish"` and copies PCF output into `out\controls\publish`
Expand All @@ -227,14 +227,13 @@ Because `ProjectType=Pcf` is built on `Microsoft.NET.Sdk`, it also sets `EnableD

Main hooks:

- `_ScriptLibraryNodeRestore` (`AfterTargets="CollectPackageReferences"`, calls the shared `NodeRestore` target - fires on solution/repo-root `dotnet restore` too, see [NodeDependencies.md](NodeDependencies.md#verb-parity))
- `BuildTypeScript` (`BeforeTargets="Build"` - delegates to Rush's own `build` command when Rush is resolved, otherwise `npm run build` directly, unchanged)
- `BuildTypeScript` (`BeforeTargets="Build"` - invokes shared `NodeBuild`: Rush when it owns build, otherwise the selected package manager)
- `CleanScriptLibrary` (`AfterTargets="Clean"`, removes the TypeScript output folder only - never `node_modules`)
- `CopyScriptLibraryMainToOutput` (`AfterTargets="Build"`)
- `GetScriptLibraryOutputs`
- `GetSuppressedScriptLibraryReferences`

The package expects TypeScript sources under `$(TypeScriptDir)` (default `$(MSBuildProjectDirectory)` itself), hydrates dependencies via the shared `NodeRestore` target (see [NodeDependencies.md](NodeDependencies.md)), builds via Rush delegation or `npm run build` (see [NodeDependencies.md](NodeDependencies.md#build-delegation-to-rush)), copies the selected main JS file to `$(TargetDir)`, and lets Solution builds query which referenced script libraries are `CompileOnly` and therefore should not be deployed as separate web resources. Standalone `npm` packaging of a ScriptLibrary is planned but not yet implemented, so it does not currently set `IsPackable=false`.
The package expects sources under `$(NodeRootPath)` (default: project directory itself), hydrates dependencies via the shared `NodeRestore` target (see [NodeDependencies.md](NodeDependencies.md)), builds through the selected orchestrator or package manager (see [NodeDependencies.md](NodeDependencies.md#build-delegation-to-rush)), copies the selected main JS file to `$(TargetDir)`, and lets Solution builds query which referenced script libraries are `CompileOnly` and therefore should not be deployed as separate web resources. Standalone `npm` packaging of a ScriptLibrary is planned but not yet implemented, so it does not currently set `IsPackable=false`.

### CodeApp

Expand All @@ -243,14 +242,14 @@ The package expects TypeScript sources under `$(TypeScriptDir)` (default `$(MSBu
Main hooks:

- `CheckCodeAppPrereqs` (Node.js presence only - package manager presence is left to `NodeRestore`)
- `_CodeAppNodeRestore` (`AfterTargets="CollectPackageReferences"`, calls the shared `NodeRestore` target - fires on solution/repo-root `dotnet restore` too, see [NodeDependencies.md](NodeDependencies.md#verb-parity))
- `BuildCodeApp` (`BeforeTargets="Build"` - delegates to Rush's own `build` command when Rush is resolved, otherwise `npm run build` directly, unchanged)
- the SDK-level `_NodeRestoreAnchor` handles bare restore, while `BuildCodeApp` also depends on `NodeRestore` as a cold-cache safety net
- `BuildCodeApp` (`BeforeTargets="Build"` - invokes shared `NodeBuild`: Rush when it owns build, otherwise the selected package manager)
- `CleanCodeApp` (`AfterTargets="Clean"`, removes `dist` only - never `node_modules`)
- `CopyCodeAppDist` (`AfterTargets="Build"`)
- `GetCodeAppOutputs`
- `CopyCodeAppDistPublish` (`AfterTargets="Publish"`)

The package hydrates dependencies via the shared `NodeRestore` target (see [NodeDependencies.md](NodeDependencies.md)), builds via Rush delegation or `npm run build` (see [NodeDependencies.md](NodeDependencies.md#build-delegation-to-rush)), expects output under `dist/`, copies it into `$(OutputPath)$(AppName)/` and `$(PublishDir)$(AppName)/`, and exposes the `dist` folder plus `power.config.json` to Solution packaging. CodeApp projects are not standalone components, so the package sets `IsPackable=false` and hooks `$(BeforePack)` with `_ErrorOnCodeAppPack`, which raises a hard error before any nuspec/nupkg work starts.
The package hydrates dependencies via the shared `NodeRestore` target (see [NodeDependencies.md](NodeDependencies.md)), builds through the selected orchestrator or package manager (see [NodeDependencies.md](NodeDependencies.md#build-delegation-to-rush)), expects output under `dist/`, copies it into `$(OutputPath)$(AppName)/` and `$(PublishDir)$(AppName)/`, and exposes the `dist` folder plus `power.config.json` to Solution packaging. CodeApp projects are not standalone components, so the package sets `IsPackable=false` and hooks `$(BeforePack)` with `_ErrorOnCodeAppPack`, which raises a hard error before any nuspec/nupkg work starts.

### GenPage

Expand Down
112 changes: 112 additions & 0 deletions docs/MSBuildConventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# MSBuild Conventions

This document describes naming, layout, and extension conventions used by the TALXIS DevKit Build SDK.

## Naming

| Kind | Pattern | Example |
|---|---|---|
| Public entry target | `<Module>` | `NodeToolchain`, `NodeRestore`, `NodeBuild` |
| Private implementation target | `_<Module><Provider><Verb>` | `_NodeToolchainPnpmDetect`, `_NodeRestoreRushRun` |
| Public property | `<Module><Noun>` | `NodePackageManager`, `NodeOrchestrator` |
| Private property/item | `_<Module><Provider><Noun>` | `_NodeRestoreNpmRootPath`, `_NodeToolchainRushTempRoot` |
| Public extension item | `<Module><Role><Noun>` | `NodePackageManagerCandidate`, `NodeSelectedOrchestrator`, `NodeBuildArgument` |
| Extension dependency property | `<Role>DetectDependsOn` | `NodePackageManagerDetectDependsOn` |

An underscore marks an implementation detail. Consumers may rely on public targets, properties, and extension items, but must not call private targets or inspect private state.

C# task classes use `<Verb><Subject>` and match their `UsingTask` name, for example `ResolveNodeToolchain`, `ResolveRushProject`, and `ExecWithRetry`.

## Node file structure

```text
Targets/
NodeToolchain.targets package-manager/orchestrator selection
NodeToolchain/
Npm.targets npm candidate detection
Pnpm.targets pnpm candidate detection
Yarn.targets Yarn candidate detection
Bun.targets Bun candidate detection
Rush.targets Rush detection and project topology
NodeRestore.targets public dependency-hydration entry point
NodeRestore/
Npm.targets npm command and incremental execution
Pnpm.targets pnpm command and incremental execution
Yarn.targets Yarn command and incremental execution
Bun.targets Bun command and incremental execution
Rush.targets Rush install/update, gate, bootstrap checks
CustomCommand.targets NodeRestoreCommand override
Retry.targets shared Rush mutex/retry target
NodeBuild.targets public Node build entry point
NodeBuild/
Npm.targets npm build provider
Pnpm.targets pnpm build provider
Yarn.targets Yarn build provider
Bun.targets Bun build provider
Rush.targets build through Rush

Tasks/Node/
ResolveNodeToolchain.cs independent role selection
ResolveRushProject.cs Rush registration and subspace topology
```

## Node toolchain extension pattern

Package managers and orchestrators are independent roles. A Rush repository can therefore resolve `pnpm` as its package manager and `rush` as its orchestrator.

External NuGet packages extend detection by appending targets to:

- `NodePackageManagerDetectDependsOn`
- `NodeOrchestratorDetectDependsOn`

A detection target adds `NodePackageManagerCandidate` or `NodeOrchestratorCandidate` items. Each item uses its identity as the public value and supplies `Priority`, `RootPath`, and `Source` metadata. Orchestrators set `OwnsRestore` and `OwnsBuild` for the current project; a selected orchestrator with both values `false` is detected but does not own either lifecycle.

```xml
<PropertyGroup>
<NodeOrchestratorDetectDependsOn>
$(NodeOrchestratorDetectDependsOn);_ContosoDetect
</NodeOrchestratorDetectDependsOn>
</PropertyGroup>
<Target Name="_ContosoDetect">
<ItemGroup Condition="Exists('$(NodeRootFullPath)/contoso.json')">
<NodeOrchestratorCandidate Include="contoso">
<Priority>250</Priority>
<RootPath>$(NodeRootFullPath)</RootPath>
<OwnsRestore>true</OwnsRestore>
<OwnsBuild>true</OwnsBuild>
<Source>$(MSBuildThisFileFullPath)</Source>
</NodeOrchestratorCandidate>
</ItemGroup>
</Target>
```

Selection rejects duplicate identities, invalid priorities, equal winning priorities, missing roots, and explicit values that do not match a registered candidate.

The public `NodeToolchain` target performs resolution. The selected candidates are exposed as read-only `NodeSelectedPackageManager` and `NodeSelectedOrchestrator` items, with all candidate metadata preserved. Providers consume these items but do not add or remove them. Built-in providers use the same normal `BeforeTargets`/`AfterTargets` hooks as external packages.

`NodeBuildArgument` items carry project-type build arguments for every provider:

```xml
<ItemGroup>
<NodeBuildArgument Include="--mode">
<Value>$(NodeBuildConfiguration)</Value>
</NodeBuildArgument>
</ItemGroup>
```

An argument forwarded through Rush additionally supplies `RushParameterName`, the exact custom parameter declared in Rush `command-line.json`. Other providers ignore that metadata.

```xml
<Target Name="_ContosoBuild"
AfterTargets="NodeBuild"
Condition="'@(NodeSelectedOrchestrator)' == 'contoso' and '@(NodeSelectedOrchestrator->'%(OwnsBuild)')' == 'true'">
<Exec WorkingDirectory="@(NodeSelectedOrchestrator->'%(RootPath)')"
Command="contoso build @(NodeBuildArgument->'%(Identity) %(Value)', ' ')" />
</Target>
```

Internal resolve/run ordering should use `DependsOnTargets`; do not create a second lifecycle abstraction.

## Cross-referencing rule

A target name should identify its source: module prefix selects the folder, provider selects the file, and verb identifies the target within that file. For example `_NodeRestoreRushRun` lives in `NodeRestore/Rush.targets`.
Loading