fix(windows): force isolated repository pnpm wrapper
This commit is contained in:
Vendored
+1
-1
@@ -39,7 +39,7 @@ Keep this log exhaustive — every divergence from upstream must be listed.
|
||||
7. **`cordis/src/*.ts` JSDoc enrichment**: added `@param`/`@returns` tags and contract documentation (disposal semantics, waterfall veto, bail conditions, error cases) across the public plugin-author surface — `Context` (class, statics, and the `Context` interface properties incl. `root`), `EventsService`, `Fiber`, `RegistryService`, `ReflectService`, `Service`, `LoggerService` and their `declare module './context.ts'` overloads. Comment-only; no code changes. Motivation: the website API-reference generator renders these docs and hard-errors on undocumented members. Retire this entry when the enrichment is upstreamed to the fork.
|
||||
8. **Transactional Loader/Include config reconciliation**: Loader imports a changed entry name before disposal, awaits lifecycle settlement, and restores the previous plugin or config when candidate application fails. Loader settlement rechecks service-gated fibers after current tasks drain, rejects failures, and leaves fibers with absent dependencies pending. Group updates start candidates concurrently, await every outcome, undo changes and additions on failure, await removal, preserve programmatic option identity, and persist direct or tree-level mutations only after success. Include reads and validates detached candidate content, applies patches to a clone, reconciles the tree, and only then commits its cached content/data; direct refresh failures propagate for the caller to contain. A non-array parse is invalid, patches re-apply on every file or Include-config update, an omitted patch list clears the overlay, and initial content falls back to `initial` only on `ENOENT`. Covered by `packages/boot/app-boot/tests/config-reload.spec.ts` and `packages/host/webserver/tests/webserver.spec.ts`.
|
||||
9. **`hmr/src/index.ts` exact config watching**: `registerConfig()` watches one absolute config path outside module roots, including a path under missing parents, serializes and coalesces refreshes, and returns an async disposer that closes the watcher and drains active work. Module watches realpath their existing base directory, attach change listeners before declaring the service ready, and use that spelling for Node module-cache identity; exact config watches realpath the deepest existing watch ancestor and restore the missing suffix. Those native paths prevent Windows short-name aliases from colliding with long-form libuv event paths while exact-config callbacks keep the requested filename. Refresh failures are normalized to `Error`, logged, and broadcast through the parallel `hmr/config-update-failed` event; observer failures are contained. Config-file changes discovered by the ordinary HMR watcher use the same serialized path. Covered by `packages/boot/app-boot/tests/hmr-config.spec.ts`.
|
||||
10. **`loader/src/repository.ts`, `loader/tsdown.config.ts`, and the `@cordisjs/plugin-loader/repository` export**: the Node-only `RepositoryCache` installs one exact dependency specifier through the bundled `pnpm@11.7.0`, single-flights callers, and atomically publishes only a prepared package plus marker under the specifier hash. The subpath stays out of the browser-reachable Loader entry. Identical specifiers permanently reuse that entry; callers change the ref/specifier for another generation. A transaction-owned `pnpm` wrapper and inherited `PNPM_CONFIG_IGNORE_WORKSPACE` make pnpm's nested Git-package install reinvoke the same bundled entry outside an enclosing source workspace, including when Windows lifecycle setup prepends its own command directory to `PATH`. The temporary command directory is removed after the child settles. The isolated workspace permits dependency build scripts because a configured repository is executable code, while the child drops ambient credential-shaped variables. Covered by `packages/boot/app-boot/tests/repository-cache.spec.ts`, including a keyless local-Git `prepack` whose package is excluded from an enclosing pnpm lockfile and obtains both its build and prepare commands from declared dependencies.
|
||||
10. **`loader/src/repository.ts`, `loader/tsdown.config.ts`, and the `@cordisjs/plugin-loader/repository` export**: the Node-only `RepositoryCache` installs one exact dependency specifier through the bundled `pnpm@11.7.0`, single-flights callers, and atomically publishes only a prepared package plus marker under the specifier hash. The subpath stays out of the browser-reachable Loader entry. Identical specifiers permanently reuse that entry; callers change the ref/specifier for another generation. A transaction-owned `pnpm` wrapper and exported `PNPM_CONFIG_IGNORE_WORKSPACE` make pnpm's nested Git-package install reinvoke the same bundled entry outside an enclosing source workspace. The child retains `PNPM_HOME` for pnpm data while removing that directory from lifecycle `PATH`, and prioritizes `.CMD` in `PATHEXT` so a later inherited pnpm executable cannot outrank the wrapper on Windows. The temporary command directory is removed after the child settles. The isolated workspace permits dependency build scripts because a configured repository is executable code, while the child drops ambient credential-shaped variables. Covered by `packages/boot/app-boot/tests/repository-cache.spec.ts`, including a keyless local-Git `prepack` whose package is excluded from an enclosing pnpm lockfile, obtains both its build and prepare commands from declared dependencies, and rejects an inherited shadow pnpm.
|
||||
11. **Vendored Node-compatible TypeScript**: marked erased imports explicitly across `cordis`, `loader`, `include`, `hmr`, and `schemastery` so Node's native TypeScript transform does not request types as runtime exports. Schemastery's source uses an ESM default export and its package declares `type: module`; its built ESM/CJS entries retain explicit `.mjs`/`.cjs` extensions.
|
||||
12. **`include/src/index.ts` patch-semantics export**: extracted the private `applyPatches` body into the exported pure function `applyEntryPatches(data, patches, warn)` (the method delegates to it) and exported the `!!js` YAML dialect as `entryListSchema`, so `dsh --dump-config` composes and prints exactly what the include would mount without booting a tree. Behavior-preserving for mounting; the extraction exists because config tooling must never reimplement (and drift from) the patch algorithm. `applyEntryPatches` also indexes each `insert`ed entry as it is added, so a later patch in the same list can configure or disable a row an earlier patch inserted; upstream built the id index once before the patch loop, leaving inserted rows silently unpatchable. That matters because `dsh` composes an empty profile root with each bundle's patch layer, the profile's and the home-level `cordis.patch.yml`, and any `--patch` overlays as sibling patch lists at one include level — patches never cross an include boundary, so surface-only rows would otherwise be unreachable from user config. Covered by `packages/boot/app-boot/tests/config-reload.spec.ts`.
|
||||
13. **`include/src/index.ts` serialized child-tree mutation and `hmr/src/index.ts` main-watcher initial-scan suppression**: every Include child-tree mutation (initial apply, refresh, `internal/update` patch re-application) runs through one per-Include queue, because the group's transactional `update` is not reentrant — two concurrent applies interleave create and rollback on the same entries and strand the Include fiber without ever settling. The HMR main watcher passes `ignoreInitial: true`: the initial scan re-announced files boot had just consumed, and its `add` for a config file refreshed an Include mid-initial-apply; once serialized, a failing initial apply's rollback disposed HMR, whose teardown drain waited on the queued refresh sitting behind that same apply — a deadlock that exited 13 with no diagnostic. `registerConfig()` keeps its own `ignoreInitial: false` watcher because a user patch layer present at registration must apply once. Covered by the patch-overlay boot-failure built-bin case in `apps/cli/tests/built-bin.e2e.ts`.
|
||||
|
||||
Vendored
+22
-3
@@ -36,16 +36,35 @@ function scrubEnvironment(environment: NodeJS.ProcessEnv = process.env): NodeJS.
|
||||
return Object.fromEntries(Object.entries(environment).filter(([name]) => !SENSITIVE_ENV_PATTERN.test(name)))
|
||||
}
|
||||
|
||||
function normalizedEnvironmentPath(value: string): string {
|
||||
const unquoted = value.startsWith('"') && value.endsWith('"') ? value.slice(1, -1) : value
|
||||
const normalized = resolve(unquoted)
|
||||
return process.platform === 'win32' ? normalized.toUpperCase() : normalized
|
||||
}
|
||||
|
||||
function installEnvironment(commandDirectory: string): NodeJS.ProcessEnv {
|
||||
const scrubbed = scrubEnvironment()
|
||||
const path = Object.entries(scrubbed).find(([name]) => name.toUpperCase() === 'PATH')?.[1]
|
||||
const pathExt = Object.entries(scrubbed).find(([name]) => name.toUpperCase() === 'PATHEXT')?.[1]
|
||||
const pnpmHome = Object.entries(scrubbed).find(([name]) => name.toUpperCase() === 'PNPM_HOME')?.[1]
|
||||
const normalizedPnpmHome = pnpmHome === undefined ? undefined : normalizedEnvironmentPath(pnpmHome)
|
||||
const inheritedPath = path === undefined ? [] : path.split(delimiter).filter((entry) => {
|
||||
return normalizedPnpmHome === undefined || normalizedEnvironmentPath(entry) !== normalizedPnpmHome
|
||||
})
|
||||
const pathExtensions = pathExt?.split(';')
|
||||
const prioritizedPathExt = pathExtensions === undefined ? undefined : [
|
||||
...pathExtensions.filter(extension => extension.toUpperCase() === '.CMD'),
|
||||
...pathExtensions.filter(extension => extension.toUpperCase() !== '.CMD'),
|
||||
].join(';')
|
||||
const withoutOverrides = Object.fromEntries(Object.entries(scrubbed).filter(([name]) => {
|
||||
return name.toUpperCase() !== 'PATH' && name.toUpperCase() !== 'PNPM_CONFIG_IGNORE_WORKSPACE'
|
||||
return !['PATH', 'PATHEXT', 'PNPM_CONFIG_IGNORE_WORKSPACE'].includes(name.toUpperCase())
|
||||
}))
|
||||
return {
|
||||
...withoutOverrides,
|
||||
PATH: [commandDirectory, ...(path === undefined ? [] : [path])].join(delimiter),
|
||||
// pnpm prepends its own command directory to lifecycle PATH on Windows.
|
||||
PATH: [commandDirectory, ...inheritedPath].join(delimiter),
|
||||
// cmd.exe tests PATHEXT before later PATH entries, so the transaction's
|
||||
// pnpm.cmd must precede an inherited pnpm executable from PNPM_HOME.
|
||||
...(prioritizedPathExt === undefined ? {} : { PATHEXT: prioritizedPathExt }),
|
||||
PNPM_CONFIG_IGNORE_WORKSPACE: 'true',
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user