refactor(#2246): centralize cross-platform path-separator handling (toPosixPath / toNativePath / posixNormalize) (#2247)
Replace every open-coded separator translation across the installer/hooks
source with named, tested seams in shell-command-projection.cts (the platform
seam), removing all hardcoded `/`+`\` from path handling:
- toPosixPath(p) — this machine's native path → POSIX (running-OS relative;
for local filesystem paths).
- toNativePath(p) — POSIX → native (collapses the win32 `/\//g,'\\'` ternary).
- posixNormalize(p)— unconditional `\`→`/`, OS-independent; for emitting paths
to a POSIX/bash TARGET (which may differ from the running
OS) and for parsing mixed-separator input.
core-utils.toPosixPath now delegates to the seam, so its 20+ existing consumers
resolve to one implementation; no duplicate helper.
- ~47 sites across runtime-hooks-surface, runtime-artifact-conversion,
runtime-artifact-install-plan, drift, init, worktree-safety,
installer-migrations, installer-migration-authoring, install-engine, surface,
verify, runtime-artifact-layout, schema-detect, check-command-router.
- Closes the latent POSIX-literal-backslash corruption class (the regex form
corrupts a POSIX path containing a literal backslash; split(path.sep) does not).
- New unit + fast-check property tests for all three helpers.
Closes #2246
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
'use strict';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import { platformWriteSync } from './shell-command-projection.cjs';
|
||||
import { platformWriteSync, posixNormalize } from './shell-command-projection.cjs';
|
||||
import { formatGsdSlash } from './runtime-slash.cjs';
|
||||
|
||||
// ─── Constants ───────────────────────────────────────────────────────────────
|
||||
@@ -79,7 +79,7 @@ type DriftCategory = 'barrel' | 'migration' | 'route' | 'new_dir';
|
||||
*/
|
||||
function classifyFile(file: unknown): DriftCategory | null {
|
||||
if (typeof file !== 'string' || !file) return null;
|
||||
const norm = file.replace(/\\/g, '/');
|
||||
const norm = posixNormalize(file);
|
||||
if (MIGRATION_RES.some((r) => r.test(norm))) return 'migration';
|
||||
if (ROUTE_RES.some((r) => r.test(norm))) return 'route';
|
||||
if (BARREL_RE.test(norm)) return 'barrel';
|
||||
@@ -95,7 +95,7 @@ function classifyFile(file: unknown): DriftCategory | null {
|
||||
* check `structureMd.includes('src/lib')` holds.
|
||||
*/
|
||||
function isPathMapped(file: string, structureMd: string): boolean {
|
||||
const norm = file.replace(/\\/g, '/');
|
||||
const norm = posixNormalize(file);
|
||||
const parts = norm.split('/');
|
||||
// Check prefixes from longest to shortest; any hit means "mapped".
|
||||
for (let i = parts.length - 1; i >= 1; i--) {
|
||||
@@ -191,7 +191,7 @@ function detectDrift(input: unknown): DetectDriftResult | SkippedResult {
|
||||
const seen = new Map<string, string>();
|
||||
|
||||
for (const rawFile of added) {
|
||||
const file = rawFile.replace(/\\/g, '/');
|
||||
const file = posixNormalize(rawFile);
|
||||
const specific = classifyFile(file);
|
||||
let category: string | null = specific;
|
||||
if (!category) {
|
||||
@@ -318,7 +318,7 @@ function chooseAffectedPaths(paths: string[]): string[] {
|
||||
const out = new Set<string>();
|
||||
for (const raw of paths || []) {
|
||||
if (typeof raw !== 'string' || !raw) continue;
|
||||
const file = raw.replace(/\\/g, '/');
|
||||
const file = posixNormalize(raw);
|
||||
const parts = file.split('/');
|
||||
if (parts.length === 0) continue;
|
||||
const top = parts[0];
|
||||
|
||||
Reference in New Issue
Block a user