diff --git a/README.md b/README.md index f194851..b44ca0f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/BuildProcess.md b/docs/BuildProcess.md index ee2765a..e1d0cb2 100644 --- a/docs/BuildProcess.md +++ b/docs/BuildProcess.md @@ -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` ``, 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` @@ -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 @@ -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 diff --git a/docs/MSBuildConventions.md b/docs/MSBuildConventions.md new file mode 100644 index 0000000..a96ecb4 --- /dev/null +++ b/docs/MSBuildConventions.md @@ -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 | `` | `NodeToolchain`, `NodeRestore`, `NodeBuild` | +| Private implementation target | `_` | `_NodeToolchainPnpmDetect`, `_NodeRestoreRushRun` | +| Public property | `` | `NodePackageManager`, `NodeOrchestrator` | +| Private property/item | `_` | `_NodeRestoreNpmRootPath`, `_NodeToolchainRushTempRoot` | +| Public extension item | `` | `NodePackageManagerCandidate`, `NodeSelectedOrchestrator`, `NodeBuildArgument` | +| Extension dependency property | `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 `` 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 + + + $(NodeOrchestratorDetectDependsOn);_ContosoDetect + + + + + + 250 + $(NodeRootFullPath) + true + true + $(MSBuildThisFileFullPath) + + + +``` + +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 + + + $(NodeBuildConfiguration) + + +``` + +An argument forwarded through Rush additionally supplies `RushParameterName`, the exact custom parameter declared in Rush `command-line.json`. Other providers ignore that metadata. + +```xml + + + +``` + +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`. diff --git a/docs/NodeDependencies.md b/docs/NodeDependencies.md index fbea5d0..8f358e6 100644 --- a/docs/NodeDependencies.md +++ b/docs/NodeDependencies.md @@ -2,21 +2,8 @@ `Pcf`, `ScriptLibrary`, and `CodeApp` projects need `node_modules` hydrated before their JavaScript/TypeScript build step runs. The SDK does this automatically via a shared `NodeRestore` MSBuild target - it detects the -right package manager for the project (npm, pnpm, Yarn, Bun, or Rush) from the same marker files those tools -themselves already use, and runs the correct install command for the situation (local dev vs. CI, mutable vs. -frozen/reproducible). - -This replaces the previous behavior of unconditionally running `npm install`, which had three problems: - -- **No opt-out** - broke any repo where dependencies are hydrated by a different tool, or by an orchestrator - like Rush that must never be bypassed (running `pnpm install` directly in a Rush repo corrupts Rush's own - state). -- **Mutated the lockfile mid-build** - `npm install` reconciles `package-lock.json` to `package.json` and can - rewrite it (format migration, re-resolved ranges). This breaks any CI cache keyed on the lockfile's hash (the - key changes between cache restore and cache save, so it can never hit again) and makes installs - non-reproducible. -- **Always ran, every build** - no once-per-workspace guarantee for monorepos with multiple PCF/ScriptLibrary/CodeApp - projects sharing one `node_modules`. +package manager (npm, pnpm, Yarn, or Bun) and optional orchestrator (currently Rush) from the same marker +files those tools use, then runs the correct install command for local development or CI. ## Design principles @@ -29,18 +16,18 @@ underlying Node tool: `publish` alone must produce the right outcome, whether a project is pure .NET, pure Node, or both. - **Incremental and full-repo adoption use the same mechanism, not separate code paths.** A single `.csproj` dropped into an otherwise-plain folder (own local `package.json`, no repo-wide orchestrator) and every project - in a repo sharing one Rush/pnpm/npm workspace both fall out of the same marker walk-up - (`GetDirectoryNameOfFileAbove`) - the incremental case simply resolves the workspace root to the project's own + in a repo sharing one Rush/pnpm/npm installation root both fall out of the same marker walk-up + (`GetDirectoryNameOfFileAbove`) - the incremental case simply resolves the package-manager root to the project's own directory because no marker is found above it. -- **Rush is the out-of-box-supported orchestrator, not the only one.** Nothing Rush-specific is hardcoded into - the shared detection/dispatch path; the two escape hatches (`NodePackageManager=` for a tool already in - the command table, `NodeRestoreCommand=` for anything else) are the intended extension surface for - a tool this SDK doesn't ship day-one support for - no plugin/adapter abstraction is introduced. -- **Hand repo-level exclusivity arbitration to the tool that already owns it - don't reinvent it.** Rush already - has its own whole-repo, fail-fast lock for `update`/`install`/`build`. Rather than building a new generic lock - file to serialize MSBuild's parallel solution builds, `NodeRestore` retries the Rush invocation with a bounded, - exponential backoff when it hits Rush's own "already running" condition - narrow, tool-specific resilience for - one confirmed failure mode, not a general-purpose mechanism imposed on every tool. +- **Toolchain roles resolve independently.** `ResolveNodeToolchain` selects one package manager and, separately, an optional orchestrator. Built-in candidates cover npm, pnpm, Yarn, Bun, and Rush; external NuGet packages can add candidates without changing SDK core. +- **Repo-level exclusivity is enforced by the SDK, informed by the tool.** Rush has its own whole-repo, + fail-fast lock for `update`/`install`/`build`, but several of its phases (the per-user pnpm bootstrap in + `~/.rush`, the lockfile copies into `common/temp`) run before that lock and are not concurrency-safe - a + parallel solution-scope restore corrupts them. `NodeRestore` therefore serializes all Rush invocations for a + workspace behind one named system mutex, and keeps a bounded, exponential-backoff retry on Rush's own + "already running" condition as a second line of defense for invocations that don't come from this SDK - + narrow, tool-specific resilience for confirmed failure modes, not a general-purpose mechanism imposed on + every tool. - **MSBuild stays the top-level orchestrator.** It decides *when*/*whether* each project builds at all and in what order (via project references, `.slnx` build graph, `-m` parallelism). Rush is only ever the primitive that the *Node-specific* portion of that work is delegated to (installing dependencies, and - for Rush @@ -52,18 +39,20 @@ underlying Node tool: | Verb | Node behavior | |---|---| | `dotnet restore` (project **or solution/repo-root**) | Hydrates Node deps via `NodeRestore`, hooked on `AfterTargets="CollectPackageReferences"` - the one per-project target NuGet's solution-level restore reliably invokes for every project, unlike `AfterTargets="Restore"` which only fires for single-project restore. This is what makes a bare `dotnet restore` at the repo root hydrate Node dependencies for every project, not just NuGet ones. | -| `dotnet build` | Hydrates (implicit restore) + builds. For non-Rush tools, `npm run build` runs directly, unchanged. For Rush-resolved projects (`Pcf`/`ScriptLibrary`/`CodeApp`), the build step itself delegates to Rush's own `build` command instead, so Rush's content-hash incremental skip and build cache apply - see "Build delegation to Rush" below. | +| `dotnet build` | Hydrates (implicit restore) + builds. Without an owning orchestrator, the selected package manager runs the build script. For Rush-owned projects (`Pcf`/`ScriptLibrary`/`CodeApp`), the build step itself delegates to Rush's own `build` command instead, so Rush's content-hash incremental skip and build cache apply - see "Build delegation to Rush" below. | | `dotnet clean` | Removes this project's own JS build-output folder only (`dist` for CodeApp, ScriptLibrary's TypeScript output folder). Never touches `node_modules` or any shared workspace state - "clean" and "prune installed deps" are different operations, and removing `node_modules` is a far more expensive, disruptive step than a normal `dotnet clean` should trigger silently. `Pcf` has no new Clean target from this SDK - Microsoft's own `PcfClean` (`npm run clean`) already owns PCF's `out/controls` cleanup. | | `dotnet publish` | Copies JS build output into the publish directory (existing, unaffected by any of the above). | -`dotnet build --no-restore` still triggers `NodeRestore` - `CollectPackageReferences` is a build-time target, not -gated by NuGet's `--no-restore` flag. This is deliberate, not a bug: worst case it's a cheap no-op via the -existing incremental gate (non-Rush) or Rush's own state hash (Rush); it never silently skips Node hydration just -because NuGet's own restore step was skipped. +`dotnet build --no-restore` still triggers `NodeRestore` - it sits in each project type's Node build chain +(`BuildTypeScript`/`PcfBuild`/`BuildCodeApp`), not only behind NuGet's restore. This is deliberate, not a bug: +worst case it's a cheap no-op via the existing incremental gate (non-Rush) or the Rush up-to-date gate; it never +silently skips Node hydration just because NuGet's own restore step was skipped. + +On a cold NuGet cache, the SDK re-evaluates each eligible Node project after package restore downloads the Tasks package, so the same `dotnet restore` invocation can run `NodeRestore`. ## Build delegation to Rush -When the resolved tool for a `Pcf`/`ScriptLibrary`/`CodeApp` project is Rush, the *build* step (not just +When `NodeOrchestrator` resolves to Rush for a `Pcf`/`ScriptLibrary`/`CodeApp` project, the *build* step (not just dependency hydration) is delegated to Rush's own `install-run-rush.js build`, instead of calling `npm run build` directly - this is what actually lets Rush's per-project content-hash incremental skip and build cache apply to the Node build step. A direct `npm run build` every time has zero incrementality of its own. @@ -84,8 +73,7 @@ choice, because a fixed choice creates one of two different regressions: A project directory under a Rush marker but not actually listed in `rush.json`'s `projects` array (legitimate incremental adoption - not every project needs to join Rush's graph on day one) is detected proactively before -either restore or build routes through Rush, and falls back to this project's own direct `npm install`/ -`npm run build` with a visible warning instead. +either restore or build routes through Rush, and falls back to the independently selected package manager for restore and build, with a visible warning instead. ### PCF-specific: forwarding the build mode as `--build-mode` @@ -160,55 +148,54 @@ without the archived-cache performance benefit. ## How detection works -Purely via MSBuild's built-in `GetDirectoryNameOfFileAbove`, walking up from the project directory (or -`$(TypeScriptDir)` for ScriptLibrary) looking for the first marker in this order: +`NodeToolchain` walks upward from `$(NodeRootFullPath)` with MSBuild's built-in `GetDirectoryNameOfFileAbove`. Package-manager and orchestrator detection are separate: -| Precedence | Marker | Resolved tool | -|---|---|---| -| 1 | `rush.json` | `rush` | -| 2 | `pnpm-lock.yaml` | `pnpm` | -| 3 | `yarn.lock` | `yarn` (Classic or Berry, detected via `.yarnrc.yml`) | -| 4 | `bun.lockb` | `bun` | -| 5 | `package-lock.json` | `npm` | -| _(none found)_ | - | `npm`, run in the project directory itself (today's default behavior for a bare project) | +| Role | Marker | Value | Priority | +|---|---|---|---| +| Package manager | `pnpm-lock.yaml` | `pnpm` | 200 | +| Package manager | `yarn.lock` | `yarn` | 190 | +| Package manager | `bun.lock` or `bun.lockb` | `bun` | 180 | +| Package manager | `package-lock.json`, or no stronger marker | `npm` | 0 | +| Orchestrator | `rush.json` | `rush` | 300 | -No bundled Node script, external npm dependency, or extra process is needed - these five filenames have been -stable across all of these tools for years, and the small number of install command variants below don't need -a library to track. +This means a Rush repository normally resolves both its underlying package manager (for example `pnpm`) and `rush`. Rush owns restore/build only for projects registered in `rush.json`; an unregistered project remains a non-owning selected orchestrator and uses the selected package-manager path. ## Configuration | Property | Default | Description | -|----------|---------|-------------| -| `NodePackageManager` | _(auto)_ | Leave empty to auto-detect via the table above. Set explicitly to `npm`, `pnpm`, `yarn`, `bun`, or `rush` to skip detection and force a tool (the workspace root is still resolved the same way). Set to `None` to skip Node restore entirely - use this when dependencies are hydrated by something external to the build (a separate CI step, a different orchestrator, etc.). | -| `NodeRestoreCommand` | _(empty)_ | Escape hatch: if set, this exact command line is run instead of anything auto-detected or resolved from `NodePackageManager` - for any tool this SDK doesn't know about, or any custom install invocation. Runs every build (no incremental caching, since an arbitrary command's staleness can't be inferred). | -| `NodeRestoreProjectDirectory` | `$(MSBuildProjectDirectory)` | Directory containing `package.json` that detection starts walking up from. ScriptLibrary sets this to `$(TypeScriptDir)` before calling `NodeRestore`; Pcf/CodeApp use the default. | -| `IsRunningInCI` | _(auto)_ | Reused as-is from [Versioning.md](Versioning.md) - leave empty to auto-detect CI from environment variables, or set `true`/`false` to override. Selects the frozen/reproducible install variant below. | +|---|---|---| +| `NodePackageManager` | _(auto)_ | Package manager: `npm`, `pnpm`, `yarn`, `bun`, or `None`. `None` skips dependency hydration. | +| `NodeOrchestrator` | _(auto)_ | Orchestrator: `rush` or `None`. `None` disables orchestrator ownership while retaining package-manager detection. | +| `NodeRestoreCommand` | _(empty)_ | Exact restore command override. It runs from `NodeRootFullPath` on every invocation and suppresses built-in restore providers. | +| `NodeRootPath` | `.` | Relative Node project root for Pcf, ScriptLibrary, and CodeApp. | +| `IsRunningInCI` | _(auto)_ | Selects frozen/reproducible install commands. | + +External package-manager detection targets append to `NodePackageManagerDetectDependsOn` and add `NodePackageManagerCandidate` items. External orchestrators use `NodeOrchestratorDetectDependsOn` and `NodeOrchestratorCandidate`. Candidates provide `Priority`, `RootPath`, and `Source`; orchestrators set `OwnsRestore` and `OwnsBuild` for the current project. The winning items are exposed as `NodeSelectedPackageManager` and `NodeSelectedOrchestrator`, with custom metadata preserved. Providers hook the public `NodeRestore` or `NodeBuild` target with normal `BeforeTargets`/`AfterTargets`, gate on those selected items, and use explicit dependencies for their own internal ordering. Built-in npm, pnpm, Yarn, Bun, and Rush providers use this same contract. + +`NodeBuildArgument` items carry arguments from the project type to every build provider. Each item identity is an argument name and its `Value` metadata is the value. A Rush-forwarded argument also sets `RushParameterName` to the matching custom parameter in `command-line.json`; other providers ignore it. + +Selection is deterministic: duplicate identities, invalid priorities, equal winning priorities, missing roots, and unmatched explicit values fail with source information. `NodeRestoreCommand` suppresses built-in provider execution and runs only the supplied command. ## Frozen (CI-safe) installs -When `IsRunningInCI` resolves to `true` **and** a lockfile exists at the resolved workspace root, `NodeRestore` +When `IsRunningInCI` resolves to `true` **and** a lockfile exists at the selected provider root, `NodeRestore` uses the frozen/reproducible install variant instead of the mutable one: | Tool | Local / mutable | CI / frozen | |---|---|---| -| `rush` | `install-run-rush.js update` | `install-run-rush.js install` | +| `rush` | `install-run-rush.js update` (scoped `install --to .` on a never-installed workspace - see [Rush specifics](#rush-specifics)) | `install-run-rush.js install` (same scoping rule) | | `pnpm` | `pnpm install` | `pnpm install --frozen-lockfile` | | `yarn` (Classic) | `yarn install` | `yarn install --frozen-lockfile` | -| `yarn` (Berry) | `yarn install` | `yarn install --immutable` | +| `yarn` (Berry) | `yarn install` | `yarn install` (Berry enforces immutable installs automatically in CI) | | `bun` | `bun install` | `bun install --frozen-lockfile` | | `npm` | `npm install` | `npm ci` | Both conditions matter: `npm ci` (and the other frozen variants) hard-fail when there is no lockfile, so CI -without a committed lockfile intentionally still falls back to the mutable variant with a build warning, -rather than breaking a build that previously worked. Locally, the frozen variant is never used - `npm ci` in +without a committed lockfile intentionally still falls back to the mutable variant with a build warning. +Locally, the frozen variant is never used - `npm ci` in particular deletes `node_modules` and reinstalls from scratch on every invocation, and fails outright on the transient `package.json`/lockfile mismatch that's normal mid-edit during local development. -This is a **behavior change for CI builds** compared to the SDK's previous unconditional `npm install`: CI -builds with a committed lockfile now get a frozen, non-mutating install. This directly fixes the lockfile -cache-invalidation and reproducibility problems described above. - Rush is always treated as "has a lockfile" for this purpose - `install-run-rush.js`'s own `install` vs. `update` verbs already enforce the same frozen-vs-mutable distinction internally. @@ -219,24 +206,48 @@ must never be bypassed. `NodeRestore` always invokes it via the version-pinned b (`/common/scripts/install-run-rush.js`), never a global `rush` binary, so the exact Rush version pinned in `rush.json` is always what runs. -Rush is invoked **unconditionally on every build** rather than being gated by a stamp file: Rush is already -self-idempotent (it compares a state hash against `common/temp/last-install.flag` and exits almost immediately -if nothing changed) and already self-serializing across concurrent invocations (its own -`common/temp/rush#.lock`). A second, custom incrementality mechanism layered on top would duplicate one -Rush already owns and would be a likely source of subtly-wrong "already restored" bugs. +### Serialization + +Rush invocations are serialized behind **one named system mutex per workspace** (held inside the +`ExecWithRetry` task, across all concurrent MSBuild node processes). Rush's own repo lock is fail-fast and +covers only part of its work: the pnpm bootstrap in the per-user `~/.rush` cache and the lockfile copies into +`common/temp` run before that lock and corrupt each other when two invocations overlap - which a parallel +solution-scope restore or build otherwise guarantees. With the mutex, the first invocation does the real work +and every queued one hits Rush's own fast path. The bounded retry on Rush's "already running" message remains +as a second line of defense for invocations that don't come from this SDK. -## Once-per-workspace execution (non-Rush tools) +### Up-to-date gate -For npm/pnpm/Yarn/Bun, `NodeRestore` runs at the detected workspace root and is gated by an MSBuild +Rush is only spawned when there is possibly something to do: when `common/temp/last-install.flag` exists, the +project's `node_modules` exist, and no registered project manifest, Rush configuration file, or pnpm patch is +newer than the flag, `NodeRestore` skips the invocation entirely. A fresh clone, deleted `node_modules`, or +edited dependency/configuration input invokes Rush, which remains authoritative for the detailed state check +and re-links missing project dependencies itself. Partially deleted Rush bootstrap or local-pnpm state fails +with a recovery command; the SDK does not delete shared Rush state. + +### Scoped restore + +With Rush subspaces enabled, project restore runs `update --to .` locally or `install --to .` in CI from the +project directory. Rush resolves the owning subspace and any required cross-subspace dependency closure. + +In a conventional workspace that has never been installed, a standalone project restore uses +`install --to .`; a mutable local restore falls back to a full `update` if that scoped install cannot satisfy +the current lockfile. Installed conventional workspaces and solution-scope restores remain unscoped because +switching between filtered and full install state forces unnecessary reinstalls. + +## Once-per-package-manager-root execution (non-Rush tools) + +For npm/pnpm/Yarn/Bun, `NodeRestore` runs at the `RootPath` of `NodeSelectedPackageManager` and is gated by an MSBuild Inputs/Outputs check (package.json + lockfile → a `.node-restore.stamp` file inside that root's `node_modules`, so deleting `node_modules` re-triggers the install and the stamp can never be committed; Yarn Berry PnP, which materializes no `node_modules`, keeps the stamp at the root), so the second, -third, ... project in the same build that shares a workspace root sees the install as already up-to-date and -skips it - the same "once per workspace, not once per project" guarantee `dotnet restore` gives per solution. +third, ... project in the same build that shares a package-manager root sees the install as already up-to-date and +skips it - the same "once per installation root, not once per project" guarantee `dotnet restore` gives per solution. Known limitation: concurrent multi-proc MSBuild builds (`dotnet build -m`) of independent projects sharing one -workspace root can still race to invoke install simultaneously for these tools, since none of npm/pnpm/Yarn/Bun -ship a cross-process lock of their own (Rush does not have this problem - see above). +package-manager root can still race to invoke install simultaneously for these tools, since none of npm/pnpm/Yarn/Bun +ship a cross-process lock of their own (Rush does not have this problem - its invocations are serialized behind +the per-workspace mutex, see [Rush specifics](#rush-specifics)). ## No global side effects @@ -245,10 +256,8 @@ ship a cross-process lock of their own (Rush does not have this problem - see ab command itself fails with a normal "command not found"-style error - the same philosophy as `dotnet restore` erroring on a missing SDK/tool rather than trying to fix the environment. -Rush's own `install-run-rush.js` downloads the `rush.json`-pinned Rush release into Rush's own per-user cache -(not a global npm package, not added to `PATH`). This is Rush's own pre-existing, documented mechanism - -identical to what already happens the moment a developer manually runs `rush update` in that repo today. It is -not a new side effect introduced by this SDK. +Rush's own `install-run-rush.js` downloads the `rush.json`-pinned Rush release into Rush's per-user cache +without installing a global npm package or modifying `PATH`. ## Interop with Microsoft's PCF build SDK (`Pcf` package only) @@ -264,11 +273,3 @@ NuGet imports before the consumer's project body and before Microsoft's own defa Microsoft's original unconditional `npm install` behavior back instead, set `true` in your own project - that value is set early enough to still win over both defaults. - -## Breaking change - -The previous `NpmInstall` target (Pcf) and inline `npm install` steps (ScriptLibrary, CodeApp) are replaced -outright by the shared `NodeRestore` target - there is no back-compat alias for the old `NpmInstall` name. If -you had a workaround hooking `BeforeTargets`/`AfterTargets="NpmInstall"` (for example, redefining it as a -no-op to opt out, as described in [#90](https://github.com/TALXIS/tools-devkit-build/issues/90)), replace it -with `NodePackageManager=None` instead, and remove the old workaround target entirely. diff --git a/src/Dataverse/CodeApp/README.md b/src/Dataverse/CodeApp/README.md index 37e2b28..ab57406 100644 --- a/src/Dataverse/CodeApp/README.md +++ b/src/Dataverse/CodeApp/README.md @@ -1,6 +1,6 @@ # TALXIS.DevKit.Build.Dataverse.CodeApp -MSBuild integration for Power Apps code-first canvas app projects. Automates Node dependency restore (auto-detected package manager - npm, pnpm, Yarn, Bun, or Rush; see [NodeDependencies.md](../../../docs/NodeDependencies.md)) followed by `npm run build`, copies the compiled `dist/` output into the correct location, and exposes metadata targets that allow Solution projects to discover, generate `.meta.xml`, and package canvas apps into the solution `.zip`. +MSBuild integration for Power Apps code-first canvas app projects. Automates Node dependency restore (auto-detected package manager (npm, pnpm, Yarn, or Bun) plus optional Rush orchestration; see [NodeDependencies.md](../../../docs/NodeDependencies.md)) followed by the shared `NodeBuild` target, copies the compiled `dist/` output into the correct location, and exposes metadata targets that allow Solution projects to discover, generate `.meta.xml`, and package canvas apps into the solution `.zip`. ## Installation @@ -21,9 +21,9 @@ Or use the SDK approach: ## Prerequisites -- **Node.js** and **npm** must be available in `PATH`. +- **Node.js** and the selected package manager must be available in `PATH`. - A `package.json` must exist in the project root. -- The `npm run build` script must produce output in a `dist/` folder. +- The package `build` script must produce output in a `dist/` folder. - A `power.config.json` file must exist, describing the app schema name and metadata used by `GenerateCodeAppMetaXml`. ## How It Works @@ -31,7 +31,7 @@ Or use the SDK approach: ### Build-time targets 1. **CheckCodeAppPrereqs** -- validates that `package.json` exists and that `node` is available in PATH (package manager presence is checked by `NodeRestore` itself, since it depends on what's detected). Runs only when `RunNodeBuild` is `true` (auto-detected from the presence of `package.json`). -2. **BuildCodeApp** (runs before `Build`, depends on `CheckCodeAppPrereqs`) -- calls the shared `NodeRestore` target (auto-detected package manager) followed by `npm run build` in the project root directory. +2. **BuildCodeApp** (runs before `Build`, depends on `CheckCodeAppPrereqs`) -- calls the shared `NodeRestore` target (auto-detected package manager) followed by the shared `NodeBuild` target in the project root directory. 3. **CopyCodeAppDist** (runs after `Build`) -- copies the `dist/` folder to `$(OutputPath)$(AppName)\`. Fails the build if `dist/` is missing or if `AppName` is not set. 4. **CopyCodeAppDistPublish** (runs after `Publish`) -- same as above, but copies to `$(PublishDir)` instead. @@ -60,6 +60,8 @@ The CodeApp reference is automatically filtered out of the standard `ResolveProj | `ProjectType` | `CodeApp` | Marks the project as a code app for reference discovery. | | `AppName` | _(required)_ | Application name; used as the output folder name and in `.meta.xml` generation. | | `RunNodeBuild` | Auto-detected | Set to `true` if `package.json` exists in project root; set explicitly to override. | +| `NodePackageManager` | Auto-detected | `npm`, `pnpm`, `yarn`, `bun`, or `None`. | +| `NodeOrchestrator` | Auto-detected | `rush` or `None`. | ## power.config.json diff --git a/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets b/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets index ea9c3a5..b61ef6a 100644 --- a/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets +++ b/src/Dataverse/CodeApp/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.CodeApp.targets @@ -2,49 +2,44 @@ - true + true false - + - - - - + - - <_NodeBuildProjectDirectory>$(MSBuildProjectDirectory) - <_NodeBuildModeArgName Condition="'$(_NodeBuildModeArgName)'==''">mode - <_NodeBuildExtraArgs> - <_NodeBuildRequiredRushParams> - + + + $(NodeBuildConfiguration) + --mode + + - + - + - + - + @@ -97,9 +92,9 @@ DependsOnTargets="Build" Returns="@(_CodeAppOutputs)"> - <_CodeAppOutputs Include="$(MSBuildProjectDirectory)/dist"> + <_CodeAppOutputs Include="$(NodeRootFullPath)/dist"> $(AppName) - $(MSBuildProjectDirectory)/power.config.json + $(NodeRootFullPath)/power.config.json @@ -110,10 +105,10 @@ - + - + diff --git a/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets b/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets index 53010f0..1968634 100644 --- a/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets +++ b/src/Dataverse/Pcf/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Pcf.targets @@ -27,19 +27,11 @@ - - - - + - <_NodeBuildProjectDirectory>$(MSBuildProjectDirectory) - <_NodeBuildModeArgName>build-mode $(PcfBuildMode) + + + $(NodeBuildConfiguration) + --build-mode + + + DependsOnTargets="NodeRestore;_PcfSetNodeBuildArgs;NodeBuild"> - $(TypeScriptDir) - - - - - - - - - <_NodeBuildProjectDirectory>$(TypeScriptDir) - <_NodeBuildExtraArgs> - <_NodeBuildRequiredRushParams> - - + - + - + - - + + - - + + PreserveNewest @@ -90,8 +65,8 @@ - <_ScriptLibraryMainFile Include="$(TypeScriptDir)/**/$(ScriptLibraryName).js" - Exclude="$(TypeScriptDir)/node_modules/**;$(TypeScriptDir)/bin/**;$(TypeScriptDir)/obj/**" /> + <_ScriptLibraryMainFile Include="$(NodeRootFullPath)/**/$(ScriptLibraryName).js" + Exclude="$(NodeRootFullPath)/node_modules/**;$(NodeRootFullPath)/bin/**;$(NodeRootFullPath)/obj/**" /> diff --git a/src/Dataverse/Tasks/Tasks/ExecWithRetry.cs b/src/Dataverse/Tasks/Tasks/ExecWithRetry.cs index d201429..e8a0f6d 100644 --- a/src/Dataverse/Tasks/Tasks/ExecWithRetry.cs +++ b/src/Dataverse/Tasks/Tasks/ExecWithRetry.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Diagnostics; using System.Globalization; +using System.Security.Cryptography; using System.Text; using System.Threading; using Microsoft.Build.Framework; @@ -16,7 +17,7 @@ public class ExecWithRetry : Task, ICancelableTask { private static readonly int[] DefaultDelaysMilliseconds = { 1000, 2000, 4000, 8000, 16000 }; - private const int DefaultMaxAttempts = 45; + private const int DefaultMaxAttempts = 10; private const int DefaultFallbackDelayMilliseconds = 30000; private readonly CancellationTokenSource cancellationSource = new(); @@ -51,6 +52,12 @@ public class ExecWithRetry : Task, ICancelableTask public int FallbackDelayMilliseconds { get; set; } = DefaultFallbackDelayMilliseconds; + /// + /// Optional system-wide mutex name held for the whole run including retries. Serializes + /// commands sharing the name across processes (Rush tolerates no concurrent invocations). + /// + public string MutexName { get; set; } = string.Empty; + /// /// Signals cancellation to the running command and interrupts any backoff delay immediately. /// @@ -73,11 +80,20 @@ public void Cancel() public override bool Execute() { + Mutex mutex = null; + var mutexAcquired = false; try { var delays = ParseDelays(DelaysMilliseconds); var attempt = 0; + if (!string.IsNullOrEmpty(MutexName)) + { + var platformMutexName = CreatePlatformMutexName(MutexName); + mutex = new Mutex(initiallyOwned: false, platformMutexName); + mutexAcquired = AcquireMutex(mutex, platformMutexName); + } + while (true) { cancellationSource.Token.ThrowIfCancellationRequested(); @@ -119,6 +135,53 @@ public override bool Execute() { return false; } + finally + { + if (mutexAcquired) + { + try + { + mutex.ReleaseMutex(); + } + catch (ApplicationException) + { + } + } + mutex?.Dispose(); + } + } + + private bool AcquireMutex(Mutex mutex, string mutexName) + { + var waitLogged = false; + while (true) + { + cancellationSource.Token.ThrowIfCancellationRequested(); + try + { + if (mutex.WaitOne(250)) + { + return true; + } + } + catch (AbandonedMutexException) + { + // Previous holder died without releasing - ownership transferred to us. + return true; + } + + if (!waitLogged) + { + waitLogged = true; + Log.LogMessage(MessageImportance.Normal, $"Waiting for another serialized command holding mutex '{mutexName}' to finish..."); + } + } + } + + private static string CreatePlatformMutexName(string coordinationKey) + { + var hash = SHA256.HashData(Encoding.UTF8.GetBytes(coordinationKey)); + return $"TALXIS.Node.{Convert.ToHexString(hash)}"; } private static int[] ParseDelays(string raw) diff --git a/src/Dataverse/Tasks/Tasks/GenerateGitVersion.cs b/src/Dataverse/Tasks/Tasks/GenerateGitVersion.cs index 0b9e9e2..11c88e5 100644 --- a/src/Dataverse/Tasks/Tasks/GenerateGitVersion.cs +++ b/src/Dataverse/Tasks/Tasks/GenerateGitVersion.cs @@ -26,6 +26,7 @@ public class GenerateGitVersion : Task public string GitVersionNumberBranchPrefixes { get; set; } // e.g. "develop:1;feature/*:3;hotfix/*:4" public string GitVersionNumberProductionBranches { get; set; } // e.g. "main;master;hotfix/*;release/*" public string LocalBuildVersionNumber { get; set; } + [Required] public string IsRunningInCI { get; set; } public string GitVersionBranch { get; set; } @@ -52,7 +53,13 @@ public override bool Execute() return true; } - if (!DetectIsRunningInCI()) + if (!bool.TryParse(IsRunningInCI, out var isRunningInCI)) + { + Log.LogError($"IsRunningInCI value '{IsRunningInCI}' is not a valid boolean."); + return false; + } + + if (!isRunningInCI) { Log.LogMessage(MessageImportance.High, "Not running in CI; using LocalBuildVersionNumber."); VersionOutput = LocalBuildVersionNumber; @@ -337,57 +344,6 @@ private bool TryFindGitRoot(string path, out string gitRoot) gitRoot = null; return false; } - private bool DetectIsRunningInCI() - { - if (!string.IsNullOrEmpty(IsRunningInCI)) - { - if (bool.TryParse(IsRunningInCI, out var overrideValue)) - { - Log.LogMessage(MessageImportance.High, $"IsRunningInCI overridden to: {overrideValue}"); - return overrideValue; - } - Log.LogWarning($"IsRunningInCI value '{IsRunningInCI}' is not a valid boolean; falling back to auto-detection."); - } - - // Boolean-style vars: only treat explicit "true" as CI - var booleanCiVars = new[] - { - "CI", // Generic (GitHub Actions, GitLab, Travis, CircleCI, etc.) - "TF_BUILD", // Azure DevOps - "GITHUB_ACTIONS", // GitHub Actions - "GITLAB_CI", // GitLab CI - "CIRCLECI", // CircleCI - }; - - foreach (var varName in booleanCiVars) - { - var value = Environment.GetEnvironmentVariable(varName); - if (bool.TryParse(value, out var boolValue) && boolValue) - { - Log.LogMessage(MessageImportance.High, $"CI environment detected via {varName}"); - return true; - } - } - - // Non-boolean vars: any non-empty value indicates CI - var presenceCiVars = new[] - { - "JENKINS_URL", // Jenkins - "TEAMCITY_VERSION" // TeamCity - }; - - foreach (var varName in presenceCiVars) - { - if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable(varName))) - { - Log.LogMessage(MessageImportance.High, $"CI environment detected via {varName}"); - return true; - } - } - - Log.LogMessage(MessageImportance.High, "No CI environment detected; treating as local build."); - return false; - } private void RetrieveAllProjectReferences(string projectPath, List projects) { var projectFile = ProjectReferenceHelper.FindProjectFile(projectPath); diff --git a/src/Dataverse/Tasks/Tasks/Node/ResolveNodeToolchain.cs b/src/Dataverse/Tasks/Tasks/Node/ResolveNodeToolchain.cs new file mode 100644 index 0000000..a95f5a0 --- /dev/null +++ b/src/Dataverse/Tasks/Tasks/Node/ResolveNodeToolchain.cs @@ -0,0 +1,148 @@ +using System; +using System.Globalization; +using System.Linq; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +/// +/// Resolves the package manager and orchestrator that govern a Node project. +/// +public sealed class ResolveNodeToolchain : Task +{ + [Required] + public ITaskItem[] PackageManagerCandidates { get; set; } = Array.Empty(); + + public ITaskItem[] OrchestratorCandidates { get; set; } = Array.Empty(); + + public string RequestedPackageManager { get; set; } = string.Empty; + + public string RequestedOrchestrator { get; set; } = string.Empty; + + [Output] + public ITaskItem PackageManager { get; private set; } + + [Output] + public ITaskItem Orchestrator { get; private set; } + + public override bool Execute() + { + PackageManager = SelectCandidate( + PackageManagerCandidates, + RequestedPackageManager, + "Node package manager", + required: !IsNone(RequestedPackageManager)); + + Orchestrator = SelectCandidate( + OrchestratorCandidates, + RequestedOrchestrator, + "Node orchestrator", + required: false); + + return !Log.HasLoggedErrors; + } + + private ITaskItem SelectCandidate( + ITaskItem[] candidates, + string requestedName, + string roleName, + bool required) + { + if (IsNone(requestedName)) + { + return null; + } + + var duplicate = candidates + .GroupBy(candidate => candidate.ItemSpec, StringComparer.OrdinalIgnoreCase) + .FirstOrDefault(group => group.Count() > 1); + if (duplicate != null) + { + Log.LogError( + $"{roleName} '{duplicate.Key}' was registered more than once: " + + string.Join(", ", duplicate.Select(DescribeSource))); + return null; + } + + var eligible = string.IsNullOrWhiteSpace(requestedName) + ? candidates + : candidates.Where(candidate => + string.Equals(candidate.ItemSpec, requestedName, StringComparison.OrdinalIgnoreCase)).ToArray(); + + if (eligible.Length == 0) + { + if (!required && string.IsNullOrWhiteSpace(requestedName)) + { + return null; + } + + var registeredText = candidates.Length == 0 + ? "none" + : string.Join(", ", candidates.Select(candidate => candidate.ItemSpec)); + var requestedText = string.IsNullOrWhiteSpace(requestedName) + ? $"No registered {roleName.ToLowerInvariant()} matched this project." + : $"Requested {roleName.ToLowerInvariant()} '{requestedName}' did not match this project."; + Log.LogError($"{requestedText} Registered candidates: {registeredText}."); + return null; + } + + var ranked = eligible + .Select(candidate => new + { + Candidate = candidate, + Priority = ParsePriority(candidate, roleName) + }) + .OrderByDescending(entry => entry.Priority) + .ToArray(); + if (Log.HasLoggedErrors) + { + return null; + } + + var winner = ranked[0]; + var tied = ranked.Where(entry => entry.Priority == winner.Priority).ToArray(); + if (tied.Length > 1) + { + Log.LogError( + $"Multiple {roleName.ToLowerInvariant()} candidates matched with priority {winner.Priority}: " + + string.Join(", ", tied.Select(entry => $"{entry.Candidate.ItemSpec} ({DescribeSource(entry.Candidate)})"))); + return null; + } + + if (string.IsNullOrWhiteSpace(winner.Candidate.GetMetadata("RootPath"))) + { + Log.LogError( + $"{roleName} '{winner.Candidate.ItemSpec}' did not provide required RootPath metadata " + + $"({DescribeSource(winner.Candidate)})."); + return null; + } + + return winner.Candidate; + } + + private int ParsePriority(ITaskItem candidate, string roleName) + { + var raw = candidate.GetMetadata("Priority"); + if (string.IsNullOrWhiteSpace(raw)) + { + return 0; + } + + if (int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var priority)) + { + return priority; + } + + Log.LogError( + $"{roleName} '{candidate.ItemSpec}' has invalid Priority '{raw}' ({DescribeSource(candidate)})."); + return 0; + } + + private static bool IsNone(string value) => + string.Equals(value, "None", StringComparison.OrdinalIgnoreCase); + + private static string DescribeSource(ITaskItem candidate) + { + var source = candidate.GetMetadata("Source"); + return string.IsNullOrWhiteSpace(source) ? "unknown source" : source; + } +} diff --git a/src/Dataverse/Tasks/Tasks/Node/ResolveRushProject.cs b/src/Dataverse/Tasks/Tasks/Node/ResolveRushProject.cs new file mode 100644 index 0000000..9607b23 --- /dev/null +++ b/src/Dataverse/Tasks/Tasks/Node/ResolveRushProject.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +/// +/// Resolves a Node project against Rush's project and subspace configuration. +/// +public sealed class ResolveRushProject : Task +{ + private static readonly JsonDocumentOptions JsonOptions = new() + { + AllowTrailingCommas = true, + CommentHandling = JsonCommentHandling.Skip + }; + + [Required] + public string RushRootPath { get; set; } = string.Empty; + + [Required] + public string ProjectRoot { get; set; } = string.Empty; + + [Output] + public bool IsRegistered { get; private set; } + + [Output] + public bool SubspacesEnabled { get; private set; } + + [Output] + public string SubspaceConfigurationRoot { get; private set; } = string.Empty; + + [Output] + public string SubspaceTempRoot { get; private set; } = string.Empty; + + [Output] + public ITaskItem[] InstallPackageJsonPaths { get; private set; } = Array.Empty(); + + public override bool Execute() + { + try + { + var rushRootPath = NormalizeDirectory(RushRootPath); + var projectRoot = NormalizeDirectory(ProjectRoot); + var rushJsonPath = Path.Combine(rushRootPath, "rush.json"); + if (!File.Exists(rushJsonPath)) + { + Log.LogError($"Rush configuration was not found at '{rushJsonPath}'."); + return false; + } + + using var rushJson = JsonDocument.Parse(File.ReadAllText(rushJsonPath), JsonOptions); + if (rushJson.RootElement.ValueKind != JsonValueKind.Object) + { + Log.LogError($"Rush configuration '{rushJsonPath}' must contain a JSON object."); + return false; + } + + if (!rushJson.RootElement.TryGetProperty("projects", out var projectsElement) || + projectsElement.ValueKind != JsonValueKind.Array) + { + Log.LogError($"Rush configuration '{rushJsonPath}' does not contain a valid 'projects' array."); + return false; + } + + var projects = ReadProjects(projectsElement, rushRootPath, rushJsonPath); + if (Log.HasLoggedErrors) + { + return false; + } + + var matchingProjects = projects.Where(project => PathsEqual(project.FullPath, projectRoot)).ToArray(); + if (matchingProjects.Length > 1) + { + Log.LogError($"Rush configuration '{rushJsonPath}' registers project folder '{projectRoot}' more than once."); + return false; + } + + var currentProject = matchingProjects.SingleOrDefault(); + IsRegistered = currentProject != null; + + var subspacesJsonPath = Path.Combine(rushRootPath, "common", "config", "rush", "subspaces.json"); + var subspaceNames = new HashSet(StringComparer.Ordinal); + if (File.Exists(subspacesJsonPath)) + { + using var subspacesJson = JsonDocument.Parse(File.ReadAllText(subspacesJsonPath), JsonOptions); + if (subspacesJson.RootElement.ValueKind != JsonValueKind.Object) + { + Log.LogError($"Rush subspace configuration '{subspacesJsonPath}' must contain a JSON object."); + return false; + } + + SubspacesEnabled = ReadOptionalBoolean(subspacesJson.RootElement, "subspacesEnabled"); + if (SubspacesEnabled) + { + if (ReadOptionalBoolean(subspacesJson.RootElement, "splitWorkspaceCompatibility")) + { + Log.LogError( + $"Rush subspace configuration '{subspacesJsonPath}' enables deprecated splitWorkspaceCompatibility. " + + "Migrate the repository to standard common/config/subspaces/ configuration."); + return false; + } + + subspaceNames.Add("default"); + if (!subspacesJson.RootElement.TryGetProperty("subspaceNames", out var namesElement) || + namesElement.ValueKind != JsonValueKind.Array) + { + Log.LogError($"Rush subspace configuration '{subspacesJsonPath}' does not contain a valid 'subspaceNames' array."); + return false; + } + + foreach (var nameElement in namesElement.EnumerateArray()) + { + if (nameElement.ValueKind != JsonValueKind.String || string.IsNullOrWhiteSpace(nameElement.GetString())) + { + Log.LogError($"Rush subspace configuration '{subspacesJsonPath}' contains an invalid subspace name."); + return false; + } + subspaceNames.Add(nameElement.GetString()!); + } + } + } + + if (SubspacesEnabled) + { + foreach (var project in projects) + { + var projectSubspace = string.IsNullOrWhiteSpace(project.SubspaceName) ? "default" : project.SubspaceName; + if (!subspaceNames.Contains(projectSubspace)) + { + Log.LogError( + $"Rush project '{project.RelativePath}' references unknown subspace '{projectSubspace}'. " + + $"Register it in '{subspacesJsonPath}'."); + } + } + if (Log.HasLoggedErrors) + { + return false; + } + } + + if (!IsRegistered) + { + return true; + } + + var subspaceName = SubspacesEnabled + ? string.IsNullOrWhiteSpace(currentProject!.SubspaceName) ? "default" : currentProject.SubspaceName + : string.Empty; + + SubspaceConfigurationRoot = SubspacesEnabled + ? Path.Combine(rushRootPath, "common", "config", "subspaces", subspaceName) + : Path.Combine(rushRootPath, "common", "config", "rush"); + + SubspaceTempRoot = SubspacesEnabled + ? Path.Combine(rushRootPath, "common", "temp", subspaceName) + : Path.Combine(rushRootPath, "common", "temp"); + + InstallPackageJsonPaths = projects + .Select(project => (ITaskItem)new TaskItem(Path.Combine(project.FullPath, "package.json"))) + .ToArray(); + + return true; + } + catch (JsonException ex) + { + Log.LogError($"Invalid Rush JSON configuration: {ex.Message}"); + return false; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException) + { + Log.LogError(ex.Message); + return false; + } + } + + private List ReadProjects(JsonElement projectsElement, string workspaceRoot, string rushJsonPath) + { + var projects = new List(); + foreach (var projectElement in projectsElement.EnumerateArray()) + { + if (projectElement.ValueKind != JsonValueKind.Object || + !projectElement.TryGetProperty("projectFolder", out var folderElement) || + folderElement.ValueKind != JsonValueKind.String || + string.IsNullOrWhiteSpace(folderElement.GetString())) + { + Log.LogError($"Rush configuration '{rushJsonPath}' contains a project without a valid 'projectFolder'."); + continue; + } + + var relativePath = folderElement.GetString()!; + var subspaceName = projectElement.TryGetProperty("subspaceName", out var subspaceElement) && + subspaceElement.ValueKind == JsonValueKind.String + ? subspaceElement.GetString() ?? string.Empty + : string.Empty; + + projects.Add(new RushProject(relativePath, NormalizeDirectory(Path.Combine(workspaceRoot, relativePath)), subspaceName)); + } + return projects; + } + + private static bool ReadOptionalBoolean(JsonElement root, string propertyName) + { + return root.TryGetProperty(propertyName, out var value) && + value.ValueKind == JsonValueKind.True; + } + + private static string NormalizeDirectory(string path) + { + return Path.TrimEndingDirectorySeparator(Path.GetFullPath(path)); + } + + private static bool PathsEqual(string left, string right) + { + return string.Equals( + left, + right, + OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + + private sealed record RushProject(string RelativePath, string FullPath, string SubspaceName); +} diff --git a/src/Dataverse/Tasks/msbuild/buildTransitive/TALXIS.DevKit.Build.Dataverse.Tasks.targets b/src/Dataverse/Tasks/msbuild/buildTransitive/TALXIS.DevKit.Build.Dataverse.Tasks.targets index 29482aa..2f97119 100644 --- a/src/Dataverse/Tasks/msbuild/buildTransitive/TALXIS.DevKit.Build.Dataverse.Tasks.targets +++ b/src/Dataverse/Tasks/msbuild/buildTransitive/TALXIS.DevKit.Build.Dataverse.Tasks.targets @@ -1,5 +1,11 @@ + + + <_TALXISDevKitDataverseTasksImported>true + \ No newline at end of file diff --git a/src/Dataverse/Tasks/msbuild/tasks/Props/CIDetection.props b/src/Dataverse/Tasks/msbuild/tasks/Props/CIDetection.props new file mode 100644 index 0000000..6763917 --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Props/CIDetection.props @@ -0,0 +1,14 @@ + + + + true + false + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Props/ProjectPaths.props b/src/Dataverse/Tasks/msbuild/tasks/Props/ProjectPaths.props index 945d793..7c345f0 100644 --- a/src/Dataverse/Tasks/msbuild/tasks/Props/ProjectPaths.props +++ b/src/Dataverse/Tasks/msbuild/tasks/Props/ProjectPaths.props @@ -13,5 +13,15 @@ $([System.IO.Path]::GetFullPath($(MSBuildProjectDirectory)/$(SolutionRootPath))) + + + $(TypeScriptDir) + . + $([MSBuild]::NormalizePath($(MSBuildProjectDirectory), $(NodeRootPath))) + + $(NodeRootFullPath) \ No newline at end of file diff --git a/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets b/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets index 7772c33..a2439ea 100644 --- a/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets +++ b/src/Dataverse/Tasks/msbuild/tasks/TALXIS.DevKit.Build.Dataverse.Tasks.targets @@ -45,4 +45,6 @@ + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild.targets index 01dbbfc..5f0ba30 100644 --- a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild.targets +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild.targets @@ -1,58 +1,17 @@ - - + - production - development + production + development - + + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Bun.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Bun.targets new file mode 100644 index 0000000..472d85a --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Bun.targets @@ -0,0 +1,9 @@ + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Direct.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Direct.targets deleted file mode 100644 index 5fdadb8..0000000 --- a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Direct.targets +++ /dev/null @@ -1,18 +0,0 @@ - - - - - <_NodeBuildDirectCommand Condition="'$(_NodeRestoreResolvedTool)'=='pnpm'">pnpm run build -- - <_NodeBuildDirectCommand Condition="'$(_NodeRestoreResolvedTool)'=='yarn'">yarn run build - <_NodeBuildDirectCommand Condition="'$(_NodeRestoreResolvedTool)'=='bun'">bun run build - <_NodeBuildDirectCommand Condition="'$(_NodeBuildDirectCommand)'==''">npm run build -- - <_NodeBuildModeArgs Condition="'$(_NodeBuildModeArgName)'!=''">--$(_NodeBuildModeArgName) $(NodeBuildConfiguration) - - - - diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Npm.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Npm.targets new file mode 100644 index 0000000..141686d --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Npm.targets @@ -0,0 +1,9 @@ + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Pnpm.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Pnpm.targets new file mode 100644 index 0000000..5037bf6 --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Pnpm.targets @@ -0,0 +1,9 @@ + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Rush.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Rush.targets new file mode 100644 index 0000000..0b3a52b --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Rush.targets @@ -0,0 +1,76 @@ + + + + + + + <_NodeBuildRushRootPath>@(NodeSelectedOrchestrator->'%(RootPath)') + <_NodeBuildCommandLineJsonPath>$(_NodeBuildRushRootPath)/common/config/rush/command-line.json + <_NodeBuildCommandLineJsonText Condition="Exists('$(_NodeBuildCommandLineJsonPath)')">$([System.IO.File]::ReadAllText('$(_NodeBuildCommandLineJsonPath)')) + + + + <_NodeBuildRequiredRushParam Remove="@(_NodeBuildRequiredRushParam)" /> + <_NodeBuildRequiredRushParam Include="@(NodeBuildArgument)" + Condition="'%(NodeBuildArgument.RushParameterName)' != ''" /> + <_NodeBuildRequiredRushParam> + true + + <_NodeBuildMissingRushParam Remove="@(_NodeBuildMissingRushParam)" /> + <_NodeBuildMissingRushParam Include="@(_NodeBuildRequiredRushParam)" Condition="'%(_NodeBuildRequiredRushParam.IsDeclared)' != 'true'" /> + + + + + + + <_NodeBuildIsSolutionScope Condition="'$(SolutionPath)' != '' and '$(SolutionPath)' != '*Undefined*'">true + <_NodeBuildRushCommand Condition="'$(_NodeBuildIsSolutionScope)' != 'true'">node "$(_NodeBuildRushRootPath)/common/scripts/install-run-rush.js" build --to . + <_NodeBuildRushCommand Condition="'$(_NodeBuildIsSolutionScope)' == 'true'">node "$(_NodeBuildRushRootPath)/common/scripts/install-run-rush.js" build + + <_NodeBuildRushCommand>$(_NodeBuildRushCommand) @(NodeBuildArgument->'%(Identity) %(Value)', ' ') + + + + <_NodeExecRetryCommand>$(_NodeBuildRushCommand) + <_NodeExecRetryWorkingDirectory>$(NodeRootFullPath) + <_NodeExecRetryEnvironmentVariables>NODE_ENV=$(NodeBuildConfiguration) + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/RushDelegation.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/RushDelegation.targets deleted file mode 100644 index 0e0127b..0000000 --- a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/RushDelegation.targets +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - <_NodeBuildCommandLineJsonPath>$(_NodeRestoreWorkspaceRoot)/common/config/rush/command-line.json - <_NodeBuildCommandLineJsonText Condition="Exists('$(_NodeBuildCommandLineJsonPath)')">$([System.IO.File]::ReadAllText('$(_NodeBuildCommandLineJsonPath)')) - <_NodeBuildAllRequiredRushParams Condition="'$(_NodeBuildModeArgName)' != ''">--$(_NodeBuildModeArgName) - <_NodeBuildAllRequiredRushParams Condition="'$(_NodeBuildRequiredRushParams)' != '' and '$(_NodeBuildAllRequiredRushParams)' != ''">$(_NodeBuildAllRequiredRushParams);$(_NodeBuildRequiredRushParams) - <_NodeBuildAllRequiredRushParams Condition="'$(_NodeBuildAllRequiredRushParams)' == ''">$(_NodeBuildRequiredRushParams) - - - - <_NodeBuildRequiredRushParam Remove="@(_NodeBuildRequiredRushParam)" /> - <_NodeBuildRequiredRushParam Include="$(_NodeBuildAllRequiredRushParams)" /> - <_NodeBuildRequiredRushParam> - true - - <_NodeBuildMissingRushParam Remove="@(_NodeBuildMissingRushParam)" /> - <_NodeBuildMissingRushParam Include="@(_NodeBuildRequiredRushParam)" Condition="'%(_NodeBuildRequiredRushParam.IsDeclared)' != 'true'" /> - - - - - - - <_NodeBuildIsSolutionScope Condition="'$(SolutionPath)' != '' and '$(SolutionPath)' != '*Undefined*'">true - <_NodeBuildRushCommand Condition="'$(_NodeBuildIsSolutionScope)' != 'true'">node "$(_NodeRestoreWorkspaceRoot)/common/scripts/install-run-rush.js" build --to . - <_NodeBuildRushCommand Condition="'$(_NodeBuildIsSolutionScope)' == 'true'">node "$(_NodeRestoreWorkspaceRoot)/common/scripts/install-run-rush.js" build - - - <_NodeBuildRushCommand Condition="'$(_NodeBuildModeArgName)' != ''">$(_NodeBuildRushCommand) --$(_NodeBuildModeArgName) $(NodeBuildConfiguration) - <_NodeBuildRushCommand Condition="'$(_NodeBuildExtraArgs)' != ''">$(_NodeBuildRushCommand) $(_NodeBuildExtraArgs) - - - - <_NodeRestoreRetryCommand>$(_NodeBuildRushCommand) - <_NodeRestoreRetryWorkingDirectory>$(_NodeBuildProjectDirectory) - <_NodeRestoreRetryEnvironmentVariables>NODE_ENV=$(NodeBuildConfiguration) - - - - - diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Yarn.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Yarn.targets new file mode 100644 index 0000000..aab675e --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeBuild/Yarn.targets @@ -0,0 +1,9 @@ + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore.targets index e2930e6..f2908d2 100644 --- a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore.targets +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore.targets @@ -1,50 +1,25 @@ - - - - - - - - - - - + + + + + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Bun.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Bun.targets new file mode 100644 index 0000000..c67ca08 --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Bun.targets @@ -0,0 +1,32 @@ + + + + <_NodeRestoreBunRootPath>@(NodeSelectedPackageManager->'%(RootPath)') + <_NodeRestoreHasLockfile Condition="Exists('$(_NodeRestoreBunRootPath)/bun.lock') or Exists('$(_NodeRestoreBunRootPath)/bun.lockb')">true + <_NodeRestoreUseFrozen Condition="'$(IsRunningInCI)' == 'true' and '$(_NodeRestoreHasLockfile)' == 'true'">true + <_NodeRestorePackageJsonPath>$(NodeRootFullPath)/package.json + <_NodeRestoreLockfilePath Condition="Exists('$(_NodeRestoreBunRootPath)/bun.lock')">$(_NodeRestoreBunRootPath)/bun.lock + <_NodeRestoreLockfilePath Condition="'$(_NodeRestoreLockfilePath)' == '' and Exists('$(_NodeRestoreBunRootPath)/bun.lockb')">$(_NodeRestoreBunRootPath)/bun.lockb + <_NodeRestoreIncrementalInputs>$(_NodeRestorePackageJsonPath) + <_NodeRestoreIncrementalInputs Condition="'$(_NodeRestoreHasLockfile)' == 'true'">$(_NodeRestorePackageJsonPath);$(_NodeRestoreLockfilePath) + <_NodeRestoreStampPath>$(_NodeRestoreBunRootPath)/node_modules/.node-restore.stamp + <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreUseFrozen)' == 'true'">bun install --frozen-lockfile + <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedCommand)' == ''">bun install + + + + + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/CustomCommand.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/CustomCommand.targets index b68cd62..f157739 100644 --- a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/CustomCommand.targets +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/CustomCommand.targets @@ -2,11 +2,11 @@ - - diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Detection.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Detection.targets deleted file mode 100644 index 88f270f..0000000 --- a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Detection.targets +++ /dev/null @@ -1,93 +0,0 @@ - - - - <_NodeRestoreIsCI Condition="'$(IsRunningInCI)'!=''">$(IsRunningInCI) - <_NodeRestoreIsCI Condition="'$(_NodeRestoreIsCI)'=='' and ( - '$(CI)'=='true' or - '$(TF_BUILD)'=='True' or '$(TF_BUILD)'=='true' or - '$(GITHUB_ACTIONS)'=='true' or - '$(GITLAB_CI)'=='true' or - '$(CIRCLECI)'=='true' or - '$(JENKINS_URL)'!='' or - '$(TEAMCITY_VERSION)'!='')">true - <_NodeRestoreIsCI Condition="'$(_NodeRestoreIsCI)'==''">false - - - - - - $(MSBuildProjectDirectory) - - - - - <_NodeRestoreRushRoot>$([MSBuild]::GetDirectoryNameOfFileAbove('$(NodeRestoreProjectDirectory)', 'rush.json')) - <_NodeRestorePnpmRoot Condition="'$(_NodeRestoreRushRoot)'==''">$([MSBuild]::GetDirectoryNameOfFileAbove('$(NodeRestoreProjectDirectory)', 'pnpm-lock.yaml')) - <_NodeRestoreYarnRoot Condition="'$(_NodeRestoreRushRoot)'=='' and '$(_NodeRestorePnpmRoot)'==''">$([MSBuild]::GetDirectoryNameOfFileAbove('$(NodeRestoreProjectDirectory)', 'yarn.lock')) - <_NodeRestoreBunRoot Condition="'$(_NodeRestoreRushRoot)'=='' and '$(_NodeRestorePnpmRoot)'=='' and '$(_NodeRestoreYarnRoot)'==''">$([MSBuild]::GetDirectoryNameOfFileAbove('$(NodeRestoreProjectDirectory)', 'bun.lockb')) - <_NodeRestoreNpmRoot Condition="'$(_NodeRestoreRushRoot)'=='' and '$(_NodeRestorePnpmRoot)'=='' and '$(_NodeRestoreYarnRoot)'=='' and '$(_NodeRestoreBunRoot)'==''">$([MSBuild]::GetDirectoryNameOfFileAbove('$(NodeRestoreProjectDirectory)', 'package-lock.json')) - - - - - <_NodeRestoreDetectedTool Condition="'$(_NodeRestoreRushRoot)'!=''">rush - <_NodeRestoreDetectedTool Condition="'$(_NodeRestoreDetectedTool)'=='' and '$(_NodeRestorePnpmRoot)'!=''">pnpm - <_NodeRestoreDetectedTool Condition="'$(_NodeRestoreDetectedTool)'=='' and '$(_NodeRestoreYarnRoot)'!=''">yarn - <_NodeRestoreDetectedTool Condition="'$(_NodeRestoreDetectedTool)'=='' and '$(_NodeRestoreBunRoot)'!=''">bun - <_NodeRestoreDetectedTool Condition="'$(_NodeRestoreDetectedTool)'=='' and '$(_NodeRestoreNpmRoot)'!=''">npm - <_NodeRestoreDetectedTool Condition="'$(_NodeRestoreDetectedTool)'==''">npm - - <_NodeRestoreDetectedRoot Condition="'$(_NodeRestoreDetectedTool)'=='rush'">$(_NodeRestoreRushRoot) - <_NodeRestoreDetectedRoot Condition="'$(_NodeRestoreDetectedTool)'=='pnpm'">$(_NodeRestorePnpmRoot) - <_NodeRestoreDetectedRoot Condition="'$(_NodeRestoreDetectedTool)'=='yarn'">$(_NodeRestoreYarnRoot) - <_NodeRestoreDetectedRoot Condition="'$(_NodeRestoreDetectedTool)'=='bun'">$(_NodeRestoreBunRoot) - <_NodeRestoreDetectedRoot Condition="'$(_NodeRestoreDetectedTool)'=='npm' and '$(_NodeRestoreNpmRoot)'!=''">$(_NodeRestoreNpmRoot) - <_NodeRestoreDetectedRoot Condition="'$(_NodeRestoreDetectedRoot)'==''">$(NodeRestoreProjectDirectory) - - - - - <_NodeRestoreResolvedTool Condition="'$(NodePackageManager)' != ''">$(NodePackageManager) - <_NodeRestoreResolvedTool Condition="'$(_NodeRestoreResolvedTool)'==''">$(_NodeRestoreDetectedTool) - <_NodeRestoreWorkspaceRoot>$(_NodeRestoreDetectedRoot) - - - <_NodeRestoreYarnBerry Condition="'$(_NodeRestoreResolvedTool)'=='yarn' and Exists('$(_NodeRestoreWorkspaceRoot)/.yarnrc.yml')">true - - - - - <_NodeRestoreResolvedCommand>$(NodeRestoreCommand) - - - - - - - - diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Generic.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Generic.targets deleted file mode 100644 index 1f4e41f..0000000 --- a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Generic.targets +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - <_NodeRestoreHasLockfile Condition="'$(_NodeRestoreResolvedTool)'=='pnpm' and Exists('$(_NodeRestoreWorkspaceRoot)/pnpm-lock.yaml')">true - <_NodeRestoreHasLockfile Condition="'$(_NodeRestoreHasLockfile)'=='' and '$(_NodeRestoreResolvedTool)'=='yarn' and Exists('$(_NodeRestoreWorkspaceRoot)/yarn.lock')">true - <_NodeRestoreHasLockfile Condition="'$(_NodeRestoreHasLockfile)'=='' and '$(_NodeRestoreResolvedTool)'=='bun' and Exists('$(_NodeRestoreWorkspaceRoot)/bun.lockb')">true - <_NodeRestoreHasLockfile Condition="'$(_NodeRestoreHasLockfile)'=='' and '$(_NodeRestoreResolvedTool)'=='npm' and Exists('$(_NodeRestoreWorkspaceRoot)/package-lock.json')">true - <_NodeRestoreHasLockfile Condition="'$(_NodeRestoreHasLockfile)'==''">false - - <_NodeRestoreUseFrozen Condition="'$(_NodeRestoreIsCI)'=='true' and '$(_NodeRestoreHasLockfile)'=='true'">true - <_NodeRestoreUseFrozen Condition="'$(_NodeRestoreUseFrozen)'==''">false - - - - - <_NodeRestorePackageJsonPath>$(NodeRestoreProjectDirectory)/package.json - <_NodeRestoreLockfilePath Condition="'$(_NodeRestoreResolvedTool)'=='pnpm' and '$(_NodeRestoreHasLockfile)'=='true'">$(_NodeRestoreWorkspaceRoot)/pnpm-lock.yaml - <_NodeRestoreLockfilePath Condition="'$(_NodeRestoreLockfilePath)'=='' and '$(_NodeRestoreResolvedTool)'=='yarn' and '$(_NodeRestoreHasLockfile)'=='true'">$(_NodeRestoreWorkspaceRoot)/yarn.lock - <_NodeRestoreLockfilePath Condition="'$(_NodeRestoreLockfilePath)'=='' and '$(_NodeRestoreResolvedTool)'=='bun' and '$(_NodeRestoreHasLockfile)'=='true'">$(_NodeRestoreWorkspaceRoot)/bun.lockb - <_NodeRestoreLockfilePath Condition="'$(_NodeRestoreLockfilePath)'=='' and '$(_NodeRestoreResolvedTool)'=='npm' and '$(_NodeRestoreHasLockfile)'=='true'">$(_NodeRestoreWorkspaceRoot)/package-lock.json - <_NodeRestoreIncrementalInputs>$(_NodeRestorePackageJsonPath) - <_NodeRestoreIncrementalInputs Condition="'$(_NodeRestoreHasLockfile)'=='true'">$(_NodeRestorePackageJsonPath);$(_NodeRestoreLockfilePath) - <_NodeRestoreStampPath>$(_NodeRestoreWorkspaceRoot)/node_modules/.node-restore.stamp - <_NodeRestoreStampPath Condition="'$(_NodeRestoreYarnBerry)'=='true'">$(_NodeRestoreWorkspaceRoot)/.node-restore.stamp - - - - - <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedTool)'=='pnpm' and '$(_NodeRestoreUseFrozen)'!='true'">pnpm install - <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedTool)'=='pnpm' and '$(_NodeRestoreUseFrozen)'=='true'">pnpm install --frozen-lockfile - - <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedTool)'=='yarn' and '$(_NodeRestoreUseFrozen)'!='true'">yarn install - <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedTool)'=='yarn' and '$(_NodeRestoreUseFrozen)'=='true' and '$(_NodeRestoreYarnBerry)'=='true'">yarn install --immutable - <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedTool)'=='yarn' and '$(_NodeRestoreUseFrozen)'=='true' and '$(_NodeRestoreYarnBerry)'!='true'">yarn install --frozen-lockfile - - <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedTool)'=='bun' and '$(_NodeRestoreUseFrozen)'!='true'">bun install - <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedTool)'=='bun' and '$(_NodeRestoreUseFrozen)'=='true'">bun install --frozen-lockfile - - <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedTool)'=='npm' and '$(_NodeRestoreUseFrozen)'!='true'">npm install - <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedTool)'=='npm' and '$(_NodeRestoreUseFrozen)'=='true'">npm ci - - - - - - - - - - - - - diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Npm.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Npm.targets new file mode 100644 index 0000000..5b95f23 --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Npm.targets @@ -0,0 +1,34 @@ + + + + <_NodeRestoreNpmRootPath>@(NodeSelectedPackageManager->'%(RootPath)') + <_NodeRestoreHasLockfile Condition="Exists('$(_NodeRestoreNpmRootPath)/package-lock.json')">true + <_NodeRestoreUseFrozen Condition="'$(IsRunningInCI)' == 'true' and '$(_NodeRestoreHasLockfile)' == 'true'">true + <_NodeRestorePackageJsonPath>$(NodeRootFullPath)/package.json + <_NodeRestoreLockfilePath Condition="'$(_NodeRestoreHasLockfile)' == 'true'">$(_NodeRestoreNpmRootPath)/package-lock.json + <_NodeRestoreIncrementalInputs>$(_NodeRestorePackageJsonPath) + <_NodeRestoreIncrementalInputs Condition="'$(_NodeRestoreHasLockfile)' == 'true'">$(_NodeRestorePackageJsonPath);$(_NodeRestoreLockfilePath) + <_NodeRestoreStampPath>$(_NodeRestoreNpmRootPath)/node_modules/.node-restore.stamp + <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreUseFrozen)' == 'true'">npm ci + <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedCommand)' == ''">npm install + + + + + + + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Pnpm.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Pnpm.targets new file mode 100644 index 0000000..3788224 --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Pnpm.targets @@ -0,0 +1,31 @@ + + + + <_NodeRestorePnpmRootPath>@(NodeSelectedPackageManager->'%(RootPath)') + <_NodeRestoreHasLockfile Condition="Exists('$(_NodeRestorePnpmRootPath)/pnpm-lock.yaml')">true + <_NodeRestoreUseFrozen Condition="'$(IsRunningInCI)' == 'true' and '$(_NodeRestoreHasLockfile)' == 'true'">true + <_NodeRestorePackageJsonPath>$(NodeRootFullPath)/package.json + <_NodeRestoreLockfilePath Condition="'$(_NodeRestoreHasLockfile)' == 'true'">$(_NodeRestorePnpmRootPath)/pnpm-lock.yaml + <_NodeRestoreIncrementalInputs>$(_NodeRestorePackageJsonPath) + <_NodeRestoreIncrementalInputs Condition="'$(_NodeRestoreHasLockfile)' == 'true'">$(_NodeRestorePackageJsonPath);$(_NodeRestoreLockfilePath) + <_NodeRestoreStampPath>$(_NodeRestorePnpmRootPath)/node_modules/.node-restore.stamp + <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreUseFrozen)' == 'true'">pnpm install --frozen-lockfile + <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedCommand)' == ''">pnpm install + + + + + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Retry.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Retry.targets index a1c23fb..1db3f1a 100644 --- a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Retry.targets +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Retry.targets @@ -3,30 +3,19 @@ Shared retry-on-lock-contention helper, used by both the Rush install/update branch (Rush.targets) and the Rush build-delegation branch used by Pcf/ScriptLibrary/CodeApp. - Rush's own whole-repo lock (common/temp/rush#.lock) is fail-fast, not queue-based: a - second concurrent Rush invocation - install/update or build, both acquire the same lock - - errors immediately with "Another Rush command is already running in this repository" instead - of waiting its turn. That is exactly the shape multiple MSBuild projects, or a solution-level - parallel build, create once every Rush-registered project's restore and build both route - through Rush. Rather than inventing a separate cross-process lock, this target retries with - backoff whenever it recognizes Rush's own lock message: whichever invocation wins already - does the complete job for the entire Rush workspace, so a retry after the winner finishes is - either an instant no-op (Rush's own incremental-skip already satisfied) or, worst case, just - waits out real work already happening on its behalf. - - One retry budget serves both call sites (install/update and build), since a single unscoped - "rush build"/"rush update" per solution-scope invocation gives both the same risk profile: - the first invocation does the real work, every other invocation gets a fast no-op. + ExecWithRetry serializes SDK-owned Rush commands with one named system mutex per Rush root. + The bounded retry remains for contention with Rush commands started outside this SDK, which + can still trigger Rush's fail-fast "already running" error. --> - <_NodeRestoreRushLockMessage>Another Rush command is already running in this repository + <_NodeExecRushLockMessage>Another Rush command is already running in this repository - - + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Rush.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Rush.targets index 6e1f83a..dfa5100 100644 --- a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Rush.targets +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Rush.targets @@ -1,88 +1,76 @@ - - - - + - <_NodeRestoreRushWorkspaceRootNormalized>$([System.String]::Copy('$(_NodeRestoreWorkspaceRoot)').Replace('\', '/').TrimEnd('/')) - <_NodeRestoreRushProjectDirectoryNormalized>$([System.String]::Copy('$(NodeRestoreProjectDirectory)').Replace('\', '/').TrimEnd('/')) - <_NodeRestoreRushRelativeDir>$([MSBuild]::MakeRelative('$(_NodeRestoreRushWorkspaceRootNormalized)/', '$(_NodeRestoreRushProjectDirectoryNormalized)')) - <_NodeRestoreRushRelativeDir>$([System.String]::Copy('$(_NodeRestoreRushRelativeDir)').Replace('\', '/').TrimEnd('/')) - <_NodeRestoreRushJsonText Condition="Exists('$(_NodeRestoreWorkspaceRoot)/rush.json')">$([System.IO.File]::ReadAllText('$(_NodeRestoreWorkspaceRoot)/rush.json')) - <_NodeRestoreIsRushRegistered Condition="'$(_NodeRestoreRushJsonText)' != '' and $(_NodeRestoreRushJsonText.Contains('"$(_NodeRestoreRushRelativeDir)"'))">true - <_NodeRestoreIsRushRegistered Condition="'$(_NodeRestoreIsRushRegistered)' == ''">false - - - + <_NodeRestoreRushRootPath>@(NodeSelectedOrchestrator->'%(RootPath)') + <_NodeRestoreUseFrozen Condition="'$(IsRunningInCI)' == 'true'">true + <_NodeRestoreRushScoped Condition="'$(_NodeToolchainRushSubspacesEnabled)' != 'true' and ('$(SolutionPath)' == '' or '$(SolutionPath)' == '*Undefined*') and !Exists('$(_NodeToolchainRushTempRoot)/last-install.flag')">true + <_NodeRestoreRushProjectSelected Condition="'$(_NodeToolchainRushSubspacesEnabled)' == 'true' or '$(_NodeRestoreRushScoped)' == 'true'">true + <_NodeRestoreRushRunner>node "$(_NodeRestoreRushRootPath)/common/scripts/install-run-rush.js" - - - <_NodeRestoreResolvedTool>npm - <_NodeRestoreWorkspaceRoot>$(NodeRestoreProjectDirectory) + <_NodeRestoreResolvedCommand Condition="'$(_NodeToolchainRushSubspacesEnabled)' == 'true' and '$(_NodeRestoreUseFrozen)' != 'true'">$(_NodeRestoreRushRunner) update --to . + <_NodeRestoreResolvedCommand Condition="'$(_NodeToolchainRushSubspacesEnabled)' == 'true' and '$(_NodeRestoreUseFrozen)' == 'true'">$(_NodeRestoreRushRunner) install --to . + <_NodeRestoreResolvedCommand Condition="'$(_NodeToolchainRushSubspacesEnabled)' != 'true' and '$(_NodeRestoreRushScoped)' != 'true' and '$(_NodeRestoreUseFrozen)' != 'true'">$(_NodeRestoreRushRunner) update + <_NodeRestoreResolvedCommand Condition="'$(_NodeToolchainRushSubspacesEnabled)' != 'true' and '$(_NodeRestoreRushScoped)' != 'true' and '$(_NodeRestoreUseFrozen)' == 'true'">$(_NodeRestoreRushRunner) install + <_NodeRestoreResolvedCommand Condition="'$(_NodeToolchainRushSubspacesEnabled)' != 'true' and '$(_NodeRestoreRushScoped)' == 'true' and '$(_NodeRestoreUseFrozen)' == 'true'">$(_NodeRestoreRushRunner) install --to . + <_NodeRestoreResolvedCommand Condition="'$(_NodeToolchainRushSubspacesEnabled)' != 'true' and '$(_NodeRestoreRushScoped)' == 'true' and '$(_NodeRestoreUseFrozen)' != 'true'">$(_NodeRestoreRushRunner) install --to . || $(_NodeRestoreRushRunner) update + - - - <_NodeRestoreHasLockfile>true - <_NodeRestoreUseFrozen Condition="'$(_NodeRestoreIsCI)'=='true'">true - <_NodeRestoreUseFrozen Condition="'$(_NodeRestoreUseFrozen)'==''">false - - <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedCommand)'=='' and '$(_NodeRestoreUseFrozen)'!='true'">node "$(_NodeRestoreWorkspaceRoot)/common/scripts/install-run-rush.js" update - <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedCommand)'=='' and '$(_NodeRestoreUseFrozen)'=='true'">node "$(_NodeRestoreWorkspaceRoot)/common/scripts/install-run-rush.js" install + + + <_NodeRestoreRushFlagPath>$(_NodeToolchainRushTempRoot)/last-install.flag + <_NodeRestoreRushFlagTicks Condition="Exists('$(_NodeRestoreRushFlagPath)')">$([System.IO.File]::GetLastWriteTime('$(_NodeRestoreRushFlagPath)').Ticks) + <_NodeRestoreRushUpToDate Condition="'$(_NodeToolchainRushSubspacesEnabled)' != 'true' and '$(_NodeRestoreRushFlagTicks)' != '' and Exists('$(NodeRootFullPath)/node_modules')">true - - - + + <_NodeRestoreRushGateInput Remove="@(_NodeRestoreRushGateInput)" /> + <_NodeRestoreRushGateInput Include="@(_NodeToolchainRushInstallPackageJson)" /> + <_NodeRestoreRushGateInput Include="$(_NodeRestoreRushRootPath)/rush.json" /> + <_NodeRestoreRushGateInput Include="$(_NodeRestoreRushRootPath)/common/config/rush/**/*" /> + <_NodeRestoreRushGateInput Include="$(_NodeRestoreRushRootPath)/common/pnpm-patches/**/*" + Condition="'$(_NodeToolchainRushSubspacesEnabled)' != 'true'" /> + <_NodeRestoreRushGateInput Include="$(_NodeToolchainRushConfigurationRoot)/**/*" + Condition="'$(_NodeToolchainRushSubspacesEnabled)' == 'true'" /> + <_NodeRestoreRushNewerInput Remove="@(_NodeRestoreRushNewerInput)" /> + <_NodeRestoreRushNewerInput Include="@(_NodeRestoreRushGateInput)" + Condition="Exists('%(FullPath)') and $([System.IO.File]::GetLastWriteTime('%(FullPath)').Ticks) > $(_NodeRestoreRushFlagTicks)" /> + + + <_NodeRestoreRushUpToDate Condition="'@(_NodeRestoreRushNewerInput)' != ''">false + - + <_NodeRestoreRushBootstrapFlag Remove="@(_NodeRestoreRushBootstrapFlag)" /> - <_NodeRestoreRushBootstrapFlag Include="$(_NodeRestoreWorkspaceRoot)/common/temp/install-run/*/installed.flag" /> + <_NodeRestoreRushBootstrapFlag Include="$(_NodeRestoreRushRootPath)/common/temp/install-run/*/installed.flag" /> <_NodeRestoreRushBrokenBootstrap Remove="@(_NodeRestoreRushBrokenBootstrap)" /> <_NodeRestoreRushBrokenBootstrap Include="@(_NodeRestoreRushBootstrapFlag->'%(RootDir)%(Directory)')" Condition="!Exists('%(RootDir)%(Directory)node_modules')" /> + <_NodeRestoreRushGuttedEngine Remove="@(_NodeRestoreRushGuttedEngine)" /> + <_NodeRestoreRushGuttedEngine Include="$(_NodeRestoreRushRootPath)/common/temp/install-run/@microsoft+rush*/installed.flag" /> + <_NodeRestoreRushGuttedEngineDir Remove="@(_NodeRestoreRushGuttedEngineDir)" /> + <_NodeRestoreRushGuttedEngineDir Include="@(_NodeRestoreRushGuttedEngine->'%(RootDir)%(Directory)')" + Condition="Exists('%(RootDir)%(Directory)node_modules') and !Exists('%(RootDir)%(Directory)node_modules/@microsoft/rush/package.json')" /> - - + + + - <_NodeRestoreRetryCommand>$(_NodeRestoreResolvedCommand) - <_NodeRestoreRetryWorkingDirectory>$(_NodeRestoreWorkspaceRoot) + <_NodeExecRetryCommand>$(_NodeRestoreResolvedCommand) + <_NodeExecRetryWorkingDirectory>$(_NodeRestoreRushRootPath) + <_NodeExecRetryWorkingDirectory Condition="'$(_NodeRestoreRushProjectSelected)' == 'true'">$(NodeRootFullPath) - + Targets="_NodeExecWithRetry" + Condition="'$(_NodeRestoreRushUpToDate)' != 'true'" + Properties="_NodeExecRetryCommand=$(_NodeExecRetryCommand);_NodeExecRetryWorkingDirectory=$(_NodeExecRetryWorkingDirectory);_NodeExecRetryMutexName=$(_NodeToolchainRushMutexName)" /> diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Yarn.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Yarn.targets new file mode 100644 index 0000000..b369004 --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeRestore/Yarn.targets @@ -0,0 +1,35 @@ + + + + <_NodeRestoreYarnRootPath>@(NodeSelectedPackageManager->'%(RootPath)') + <_NodeRestoreYarnBerry Condition="Exists('$(_NodeRestoreYarnRootPath)/.yarnrc.yml')">true + <_NodeRestoreHasLockfile Condition="Exists('$(_NodeRestoreYarnRootPath)/yarn.lock')">true + <_NodeRestoreUseFrozen Condition="'$(IsRunningInCI)' == 'true' and '$(_NodeRestoreHasLockfile)' == 'true'">true + <_NodeRestorePackageJsonPath>$(NodeRootFullPath)/package.json + <_NodeRestoreLockfilePath Condition="'$(_NodeRestoreHasLockfile)' == 'true'">$(_NodeRestoreYarnRootPath)/yarn.lock + <_NodeRestoreIncrementalInputs>$(_NodeRestorePackageJsonPath) + <_NodeRestoreIncrementalInputs Condition="'$(_NodeRestoreHasLockfile)' == 'true'">$(_NodeRestorePackageJsonPath);$(_NodeRestoreLockfilePath) + <_NodeRestoreStampPath Condition="'$(_NodeRestoreYarnBerry)' == 'true'">$(_NodeRestoreYarnRootPath)/.node-restore.stamp + <_NodeRestoreStampPath Condition="'$(_NodeRestoreYarnBerry)' != 'true'">$(_NodeRestoreYarnRootPath)/node_modules/.node-restore.stamp + <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreYarnBerry)' == 'true'">yarn install + <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreYarnBerry)' != 'true' and '$(_NodeRestoreUseFrozen)' == 'true'">yarn install --frozen-lockfile + <_NodeRestoreResolvedCommand Condition="'$(_NodeRestoreResolvedCommand)' == ''">yarn install + + + + + + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain.targets new file mode 100644 index 0000000..70234eb --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain.targets @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Bun.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Bun.targets new file mode 100644 index 0000000..963e75a --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Bun.targets @@ -0,0 +1,22 @@ + + + $(NodePackageManagerDetectDependsOn);_NodeToolchainBunDetect + + + + + <_NodeToolchainBunLockRootPath>$([MSBuild]::GetDirectoryNameOfFileAbove('$(NodeRootFullPath)', 'bun.lock')) + <_NodeToolchainBunLockbRootPath>$([MSBuild]::GetDirectoryNameOfFileAbove('$(NodeRootFullPath)', 'bun.lockb')) + <_NodeToolchainBunRootPath>$(_NodeToolchainBunLockRootPath) + <_NodeToolchainBunRootPath Condition="'$(_NodeToolchainBunLockbRootPath)' != '' and ('$(_NodeToolchainBunLockRootPath)' == '' or $([System.String]::Copy('$(_NodeToolchainBunLockbRootPath)').Length) > $([System.String]::Copy('$(_NodeToolchainBunLockRootPath)').Length))">$(_NodeToolchainBunLockbRootPath) + + + + 180 + $(_NodeToolchainBunRootPath) + $(NodeRootFullPath) + $(MSBuildThisFileFullPath) + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Npm.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Npm.targets new file mode 100644 index 0000000..e64a36a --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Npm.targets @@ -0,0 +1,20 @@ + + + $(NodePackageManagerDetectDependsOn);_NodeToolchainNpmDetect + + + + + <_NodeToolchainNpmRootPath>$([MSBuild]::GetDirectoryNameOfFileAbove('$(NodeRootFullPath)', 'package-lock.json')) + + + + + 0 + $(_NodeToolchainNpmRootPath) + $(NodeRootFullPath) + $(MSBuildThisFileFullPath) + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Pnpm.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Pnpm.targets new file mode 100644 index 0000000..63e260a --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Pnpm.targets @@ -0,0 +1,19 @@ + + + $(NodePackageManagerDetectDependsOn);_NodeToolchainPnpmDetect + + + + + <_NodeToolchainPnpmRootPath>$([MSBuild]::GetDirectoryNameOfFileAbove('$(NodeRootFullPath)', 'pnpm-lock.yaml')) + + + + 200 + $(_NodeToolchainPnpmRootPath) + $(NodeRootFullPath) + $(MSBuildThisFileFullPath) + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Rush.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Rush.targets new file mode 100644 index 0000000..ece43e8 --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Rush.targets @@ -0,0 +1,39 @@ + + + $(NodeOrchestratorDetectDependsOn);_NodeToolchainRushDetect + + + + + <_NodeToolchainRushDetectedRootPath>$([MSBuild]::GetDirectoryNameOfFileAbove('$(NodeRootFullPath)', 'rush.json')) + + + + + + + + + + + + + + <_NodeToolchainRushRootPathNormalized>$([System.String]::Copy('$(_NodeToolchainRushDetectedRootPath)').Replace('\', '/').TrimEnd('/')) + <_NodeToolchainRushMutexName>TALXIS.Rush.$([System.String]::Copy('$(_NodeToolchainRushRootPathNormalized)').Replace('/', '_').Replace(':', '_').ToLowerInvariant()) + + + + + 300 + $(_NodeToolchainRushDetectedRootPath) + $(_NodeToolchainRushProjectRegistered) + $(_NodeToolchainRushProjectRegistered) + $(MSBuildThisFileFullPath) + + + + diff --git a/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Yarn.targets b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Yarn.targets new file mode 100644 index 0000000..75a7aca --- /dev/null +++ b/src/Dataverse/Tasks/msbuild/tasks/Targets/NodeToolchain/Yarn.targets @@ -0,0 +1,19 @@ + + + $(NodePackageManagerDetectDependsOn);_NodeToolchainYarnDetect + + + + + <_NodeToolchainYarnRootPath>$([MSBuild]::GetDirectoryNameOfFileAbove('$(NodeRootFullPath)', 'yarn.lock')) + + + + 190 + $(_NodeToolchainYarnRootPath) + $(NodeRootFullPath) + $(MSBuildThisFileFullPath) + + + + diff --git a/src/Sdk/Sdk/Sdk.GitVersioning.targets b/src/Sdk/Sdk/Sdk.GitVersioning.targets new file mode 100644 index 0000000..ee314c3 --- /dev/null +++ b/src/Sdk/Sdk/Sdk.GitVersioning.targets @@ -0,0 +1,36 @@ + + + + main;master;hotfix/*;release/*; + develop:1; + + + + + + + + diff --git a/src/Sdk/Sdk/Sdk.NodeRestore.targets b/src/Sdk/Sdk/Sdk.NodeRestore.targets new file mode 100644 index 0000000..1409a96 --- /dev/null +++ b/src/Sdk/Sdk/Sdk.NodeRestore.targets @@ -0,0 +1,82 @@ + + + + <_TALXISDevKitDataverseTasksTargets>$(_TALXISDevKitNuGetPackageRoot)talxis.devkit.build.dataverse.tasks/$(TALXISDevKitDataversePackageVersion)/buildTransitive/TALXIS.DevKit.Build.Dataverse.Tasks.targets + <_TALXISDevKitDataverseTasksAvailable Condition="Exists('$(_TALXISDevKitDataverseTasksTargets)')">true + + + + + + + <_NodeRestoreAnchorNodeRootFullPath>$(NodeRootFullPath) + <_NodeRestoreAnchorNodeRoot Condition="'$(_NodeRestoreAnchorNodeRootFullPath)' == ''">$(NodeRootPath) + <_NodeRestoreAnchorNodeRoot Condition="'$(_NodeRestoreAnchorNodeRootFullPath)' == '' and '$(_NodeRestoreAnchorNodeRoot)' == ''">$(TypeScriptDir) + <_NodeRestoreAnchorNodeRoot Condition="'$(_NodeRestoreAnchorNodeRootFullPath)' == '' and '$(_NodeRestoreAnchorNodeRoot)' == ''">. + <_NodeRestoreAnchorNodeRootFullPath Condition="'$(_NodeRestoreAnchorNodeRootFullPath)' == ''">$([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(_NodeRestoreAnchorNodeRoot)')) + <_NodeRestoreAnchorHasPackageJson Condition="Exists('$(_NodeRestoreAnchorNodeRootFullPath)/package.json')">true + + + + + + + + + + + + + diff --git a/src/Sdk/Sdk/Sdk.PackageReference.targets b/src/Sdk/Sdk/Sdk.PackageReference.targets new file mode 100644 index 0000000..1b22882 --- /dev/null +++ b/src/Sdk/Sdk/Sdk.PackageReference.targets @@ -0,0 +1,16 @@ + + + + $(TALXISDevKitDataversePackageBase).$(ProjectType) + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/src/Sdk/Sdk/Sdk.props b/src/Sdk/Sdk/Sdk.props index 82bb1a4..9f9639e 100644 --- a/src/Sdk/Sdk/Sdk.props +++ b/src/Sdk/Sdk/Sdk.props @@ -6,10 +6,16 @@ net472 TALXIS.DevKit.Build.Dataverse - + <_TALXISDevKitBuildSdkDir>$([System.IO.Path]::GetDirectoryName($(MSBuildThisFileDirectory.TrimEnd('\/')))) $([System.IO.Path]::GetFileName($(_TALXISDevKitBuildSdkDir))) + + + <_TALXISDevKitNuGetPackageRoot>$([System.IO.Path]::GetDirectoryName($([System.IO.Path]::GetDirectoryName($(_TALXISDevKitBuildSdkDir)))))/ - - main;master;hotfix/*;release/*; - develop:1; - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - + + + +