Skip to content

Normalize paths to POSIX format for cross-platform compatibility #628

@marcogomez-dev

Description

@marcogomez-dev

Is your feature request or improvement related to a problem?

Yes. This improvement resolves a critical cross-platform compatibility bug that caused Markdown content to fail rendering on Windows systems.

The core problem was inconsistent handling of file path separators: Windows uses backslashes (\) while Unix-like systems use forward slashes (/). The framework was mixing these formats in internal logic, leading to:

  1. Broken file dependency resolution.
  2. Failure to load assets (CSS/JavaScript) and partials on Markdown pages.
  3. Incorrect hierarchical path comparisons for shared assets.

This resulted in Markdown pages appearing blank or unstyled when served on a Windows platform.


Solution you'd like

Implement a comprehensive, centralized path normalization strategy to ensure all internal file system operations and path comparisons consistently use the POSIX forward slash (/) separator.

This is achieved by introducing a toPosix() utility and applying it strategically at the entry points of file system operations, and updating all path manipulation logic to rely exclusively on /.

Proposed Code Changes

File Changes Rationale
packages/nuekit/src/tools/fswalk.js Add toPosix and apply to relativePath. Normalizes paths returned by fs at the earliest point.
packages/nuekit/src/deps.js Add toPosix, normalize AUTO_INCLUDED, fix all comparisons and constructions. Ensures all dependency logic operates on consistent POSIX paths.
packages/nuekit/src/file.js Fix separator use in getFileInfo() and getURL(). Corrects file metadata and URL generation.

1. packages/nuekit/src/tools/fswalk.js

// Add toPosix normalization utility
function toPosix(path) {
  return path.split(sep).join('/')
}

async function walkDirectory(root) {
  // ... (existing code)
  for (const name of names) {
    // ... (existing code)
    if (stat.isDirectory()) {
      await walkDirectory(fullPath, fn, root)

    } else {
      let relativePath = relative(root, fullPath)
      // Normalize to POSIX path separators
      relativePath = toPosix(relativePath) // <--- CHANGE
      fn(relativePath)
    }
  }
}

2. packages/nuekit/src/deps.js

// Add toPosix utility
function toPosix(path) {
  return path.split(sep).join('/')
}

// Normalize AUTO_INCLUDED paths
const AUTO_INCLUDED = ['data', 'design', 'ui'].map(dir => toPosix(join('@shared', dir))) // <--- CHANGE

export function isDep(dep, dir) {
  // ... (existing code)
  // Shared directory check:
  if (dep.startsWith('@shared')) return true

  // Check if file is inside a SPA directory
  // Changed dir + sep to dir + '/'
  if (dir && dep.startsWith(dir + '/')) return true // <--- CHANGE
  
  // Changed dir + sep to dir + '/'
  const shared = join('@shared', dir) + '/' // <--- CHANGE
  
  // ... (existing code)
}


// Fixed hierarchical UI directory construction in getDeps()
const ui_dir = pageDir ? toPosix(join(pageDir, 'ui')) : 'ui' // <--- CHANGE


// Simplified parseDirs() function
export function parseDirs(dir) {
  // Removed normalize() and sep. Use consistent '/'
  const els = dir.split('/') // <--- CHANGE
  return els.map((el, i) => els.slice(0, i + 1).join('/'))
}

3. packages/nuekit/src/file.js

export function getFileInfo(file) {
  // ... (existing code)
  if (dir) {
    // Changed from sep to '/' for consistent split
    if (dir.includes('/')) info.basedir = dir.split('/')[0] // <--- CHANGE
    info.dir = dir
  }
  return info
}

export function getURL(dir, name) {
  // ... (existing code)

  // Changed from sep to '/', added null check
  const els = dir ? dir.split('/') : [] // <--- CHANGE
  // ... (existing code)
}

Alternatives you've considered

  1. Platform-Specific Conditional Logic: Using path.sep and checking process.platform === 'win32' throughout the code.

    • Reason for rejection: This is far more complex, difficult to maintain, and would clutter the codebase. Normalizing to POSIX is cleaner and aligns with web standards for URLs.
  2. Relying on built-in Node functions (e.g., path.normalize): These functions often revert to the native platform separator when returning a path, which would reintroduce the issue in components that rely on specific string comparisons.

    • Reason for rejection: A controlled, explicit normalization to the POSIX format (/) across the entire internal pipeline is necessary to guarantee consistency, especially for string-based comparisons and URL construction.

Additional context

The technical reasoning for this approach is cross-platform consistency. Using the POSIX forward slash (/) works on all operating systems and is the standard for web URLs. Normalizing paths early in the pipeline (at file system entry points) ensures that all downstream consumers (dependency resolution, URL generation) operate on clean, consistent data, eliminating the need for platform-specific branching.

This fix is crucial for Windows development and deployment environments and introduces no breaking changes to public APIs.

Verification: Test on both platforms:

  • Windows: Navigate to /docs/. Markdown must render correctly with all CSS/JS assets loaded.
  • Linux/macOS: Confirm no regression.
  • Console: No path-related warnings or errors.

Final note

This entire work, including the root cause analysis, the path normalization strategy, and the specific code changes, was designed and implemented with the assistance of an AI. While the solution addresses the identified cross-platform compatibility issues, it is crucial that these changes be thoroughly tested across all target platforms (Windows, Linux, macOS). Deep verification is required to ensure the robustness of the new path handling logic and to confirm that no regressions have been introduced into existing functionality.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions