cross-platform-compatibility

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Cross-Platform Compatibility

跨平台兼容性

Overview

概述

Comprehensive guide to writing code that works seamlessly across Windows, macOS, and Linux. Covers file path handling, environment detection, platform-specific features, and testing strategies.
本指南全面介绍如何编写可在Windows、macOS和Linux上无缝运行的代码,涵盖文件路径处理、环境检测、平台特定功能以及测试策略。

When to Use

适用场景

  • Building applications for multiple operating systems
  • Handling file system operations
  • Managing platform-specific dependencies
  • Detecting operating system and architecture
  • Working with environment variables
  • Building cross-platform CLI tools
  • Dealing with line endings and character encodings
  • Managing platform-specific build processes
  • 为多个操作系统构建应用
  • 处理文件系统操作
  • 管理平台特定依赖项
  • 检测操作系统与架构
  • 处理环境变量
  • 构建跨平台CLI工具
  • 处理行尾符与字符编码
  • 管理平台特定构建流程

Instructions

操作指南

1. File Path Handling

1. 文件路径处理

Node.js Path Module

Node.js Path模块

typescript
// ❌ BAD: Hardcoded paths with platform-specific separators
const configPath = 'C:\\Users\\user\\config.json';  // Windows only
const dataPath = '/home/user/data.txt';             // Unix only

// ✅ GOOD: Use path module
import path from 'path';
import os from 'os';

// Platform-independent path construction
const configPath = path.join(os.homedir(), 'config', 'app.json');
const dataPath = path.join(process.cwd(), 'data', 'users.txt');

// Resolve relative paths
const absolutePath = path.resolve('./config/settings.json');

// Get path components
const dirname = path.dirname('/path/to/file.txt');    // '/path/to'
const basename = path.basename('/path/to/file.txt');  // 'file.txt'
const extname = path.extname('/path/to/file.txt');    // '.txt'

// Normalize paths (handle .. and .)
const normalized = path.normalize('/path/to/../file.txt');  // '/path/file.txt'
typescript
// ❌ 错误示例:硬编码带有平台特定分隔符的路径
const configPath = 'C:\\Users\\user\\config.json';  // 仅适用于Windows
const dataPath = '/home/user/data.txt';             // 仅适用于Unix

// ✅ 正确示例:使用path模块
import path from 'path';
import os from 'os';

// 平台无关的路径构造
const configPath = path.join(os.homedir(), 'config', 'app.json');
const dataPath = path.join(process.cwd(), 'data', 'users.txt');

// 解析相对路径
const absolutePath = path.resolve('./config/settings.json');

// 获取路径组件
const dirname = path.dirname('/path/to/file.txt');    // '/path/to'
const basename = path.basename('/path/to/file.txt');  // 'file.txt'
const extname = path.extname('/path/to/file.txt');    // '.txt'

// 规范化路径(处理..和.)
const normalized = path.normalize('/path/to/../file.txt');  // '/path/file.txt'

Python Path Handling

Python路径处理

python
undefined
python
undefined

❌ BAD: Hardcoded separators

❌ 错误示例:硬编码分隔符

config_path = 'C:\Users\user\config.json' # Windows only data_path = '/home/user/data.txt' # Unix only
config_path = 'C:\Users\user\config.json' // 仅适用于Windows data_path = '/home/user/data.txt' // 仅适用于Unix

✅ GOOD: Use pathlib

✅ 正确示例:使用pathlib

from pathlib import Path import os
from pathlib import Path import os

Platform-independent path construction

平台无关的路径构造

config_path = Path.home() / 'config' / 'app.json' data_path = Path.cwd() / 'data' / 'users.txt'
config_path = Path.home() / 'config' / 'app.json' data_path = Path.cwd() / 'data' / 'users.txt'

Working with paths

路径操作

if config_path.exists(): content = config_path.read_text()
if config_path.exists(): content = config_path.read_text()

Get path components

获取路径组件

dirname = config_path.parent filename = config_path.name extension = config_path.suffix
dirname = config_path.parent filename = config_path.name extension = config_path.suffix

Resolve relative paths

解析相对路径

absolute_path = Path('./config/settings.json').resolve()
absolute_path = Path('./config/settings.json').resolve()

Create directories

创建目录

output_dir = Path('output') output_dir.mkdir(parents=True, exist_ok=True)
undefined
output_dir = Path('output') output_dir.mkdir(parents=True, exist_ok=True)
undefined

Go Path Handling

Go路径处理

go
package main

import (
    "os"
    "path/filepath"
)

func main() {
    // ❌ BAD: Hardcoded paths
    // configPath := "C:\\Users\\user\\config.json"

    // ✅ GOOD: Use filepath package
    homeDir, _ := os.UserHomeDir()
    configPath := filepath.Join(homeDir, "config", "app.json")

    // Get path components
    dir := filepath.Dir(configPath)
    base := filepath.Base(configPath)
    ext := filepath.Ext(configPath)

    // Clean and normalize paths
    cleaned := filepath.Clean("path/to/../file.txt")

    // Convert to absolute path
    absPath, _ := filepath.Abs("./config/settings.json")
}
go
package main

import (
    "os"
    "path/filepath"
)

func main() {
    // ❌ 错误示例:硬编码路径
    // configPath := "C:\\Users\\user\\config.json"

    // ✅ 正确示例:使用filepath包
    homeDir, _ := os.UserHomeDir()
    configPath := filepath.Join(homeDir, "config", "app.json")

    // 获取路径组件
    dir := filepath.Dir(configPath)
    base := filepath.Base(configPath)
    ext := filepath.Ext(configPath)

    // 清理并规范化路径
    cleaned := filepath.Clean("path/to/../file.txt")

    // 转换为绝对路径
    absPath, _ := filepath.Abs("./config/settings.json")
}

2. Platform Detection

2. 平台检测

Node.js Platform Detection

Node.js平台检测

typescript
// platform-utils.ts
import os from 'os';

export const Platform = {
  isWindows: process.platform === 'win32',
  isMacOS: process.platform === 'darwin',
  isLinux: process.platform === 'linux',
  isUnix: process.platform !== 'win32',

  get current(): 'windows' | 'macos' | 'linux' | 'unknown' {
    switch (process.platform) {
      case 'win32': return 'windows';
      case 'darwin': return 'macos';
      case 'linux': return 'linux';
      default: return 'unknown';
    }
  },

  get arch(): string {
    return process.arch; // 'x64', 'arm64', etc.
  },

  get homeDir(): string {
    return os.homedir();
  },

  get tempDir(): string {
    return os.tmpdir();
  }
};

// Usage
if (Platform.isWindows) {
  // Windows-specific code
  console.log('Running on Windows');
} else if (Platform.isMacOS) {
  // macOS-specific code
  console.log('Running on macOS');
} else if (Platform.isLinux) {
  // Linux-specific code
  console.log('Running on Linux');
}

// Architecture detection
if (Platform.arch === 'arm64') {
  console.log('Running on ARM architecture');
}
typescript
// platform-utils.ts
import os from 'os';

export const Platform = {
  isWindows: process.platform === 'win32',
  isMacOS: process.platform === 'darwin',
  isLinux: process.platform === 'linux',
  isUnix: process.platform !== 'win32',

  get current(): 'windows' | 'macos' | 'linux' | 'unknown' {
    switch (process.platform) {
      case 'win32': return 'windows';
      case 'darwin': return 'macos';
      case 'linux': return 'linux';
      default: return 'unknown';
    }
  },

  get arch(): string {
    return process.arch; // 'x64', 'arm64', etc.
  },

  get homeDir(): string {
    return os.homedir();
  },

  get tempDir(): string {
    return os.tmpdir();
  }
};

// 使用示例
if (Platform.isWindows) {
  // Windows特定代码
  console.log('运行在Windows系统上');
} else if (Platform.isMacOS) {
  // macOS特定代码
  console.log('运行在macOS系统上');
} else if (Platform.isLinux) {
  // Linux特定代码
  console.log('运行在Linux系统上');
}

// 架构检测
if (Platform.arch === 'arm64') {
  console.log('运行在ARM架构上');
}

Python Platform Detection

Python平台检测

python
undefined
python
undefined

platform_utils.py

platform_utils.py

import platform import sys
class Platform: @staticmethod def is_windows(): return sys.platform.startswith('win')
@staticmethod
def is_macos():
    return sys.platform == 'darwin'

@staticmethod
def is_linux():
    return sys.platform.startswith('linux')

@staticmethod
def is_unix():
    return not Platform.is_windows()

@staticmethod
def current():
    if Platform.is_windows():
        return 'windows'
    elif Platform.is_macos():
        return 'macos'
    elif Platform.is_linux():
        return 'linux'
    return 'unknown'

@staticmethod
def arch():
    return platform.machine()  # 'x86_64', 'arm64', etc.

@staticmethod
def version():
    return platform.version()
import platform import sys
class Platform: @staticmethod def is_windows(): return sys.platform.startswith('win')
@staticmethod
def is_macos():
    return sys.platform == 'darwin'

@staticmethod
def is_linux():
    return sys.platform.startswith('linux')

@staticmethod
def is_unix():
    return not Platform.is_windows()

@staticmethod
def current():
    if Platform.is_windows():
        return 'windows'
    elif Platform.is_macos():
        return 'macos'
    elif Platform.is_linux():
        return 'linux'
    return 'unknown'

@staticmethod
def arch():
    return platform.machine()  // 'x86_64', 'arm64', etc.

@staticmethod
def version():
    return platform.version()
// 使用示例 if Platform.is_windows(): // Windows特定代码 print('Running on Windows') elif Platform.is_macos(): // macOS特定代码 print('Running on macOS') elif Platform.is_linux(): // Linux特定代码 print('Running on Linux')
undefined

Usage

3. 行尾符处理

if Platform.is_windows(): # Windows-specific code print('Running on Windows') elif Platform.is_macos(): # macOS-specific code print('Running on macOS') elif Platform.is_linux(): # Linux-specific code print('Running on Linux')
undefined
typescript
// line-endings.ts
import os from 'os';

export const LineEnding = {
  LF: '\n',      // Unix/Linux/macOS
  CRLF: '\r\n',  // Windows
  CR: '\r',      // 旧版Mac(OS X之前)

  get platform(): string {
    return os.EOL; // 返回平台特定的行尾符
  },

  normalize(text: string, target: string = os.EOL): string {
    // 将所有行尾符规范化为目标格式
    return text.replace(/\r\n|\r|\n/g, target);
  },

  toUnix(text: string): string {
    return this.normalize(text, this.LF);
  },

  toWindows(text: string): string {
    return this.normalize(text, this.CRLF);
  }
};

// 使用示例
const fileContent = fs.readFileSync('file.txt', 'utf8');

// 规范化为平台特定的行尾符
const normalized = LineEnding.normalize(fileContent);

// 强制转换为Unix行尾符(适用于Git等场景)
const unixContent = LineEnding.toUnix(fileContent);

// 使用平台特定行尾符写入文件
fs.writeFileSync('output.txt', normalized);

3. Line Endings

4. 环境变量处理

typescript
// line-endings.ts
import os from 'os';

export const LineEnding = {
  LF: '\n',      // Unix/Linux/macOS
  CRLF: '\r\n',  // Windows
  CR: '\r',      // Old Mac (pre-OS X)

  get platform(): string {
    return os.EOL; // Returns platform-specific line ending
  },

  normalize(text: string, target: string = os.EOL): string {
    // Normalize all line endings to target
    return text.replace(/\r\n|\r|\n/g, target);
  },

  toUnix(text: string): string {
    return this.normalize(text, this.LF);
  },

  toWindows(text: string): string {
    return this.normalize(text, this.CRLF);
  }
};

// Usage
const fileContent = fs.readFileSync('file.txt', 'utf8');

// Normalize to platform-specific line endings
const normalized = LineEnding.normalize(fileContent);

// Force Unix line endings (for git, etc.)
const unixContent = LineEnding.toUnix(fileContent);

// Write with platform-specific line endings
fs.writeFileSync('output.txt', normalized);
typescript
// env-utils.ts
export class EnvUtils {
  // 获取环境变量,支持默认值
  static get(key: string, defaultValue?: string): string | undefined {
    return process.env[key] || defaultValue;
  }

  // 获取PATH分隔符(Unix为:,Windows为;)
  static get pathSeparator(): string {
    return process.platform === 'win32' ? ';' : ':';
  }

  // 将PATH分割为数组
  static getPaths(): string[] {
    const pathVar = process.env.PATH || '';
    return pathVar.split(this.pathSeparator);
  }

  // 获取常用路径
  static get home(): string {
    return process.env.HOME || process.env.USERPROFILE || '';
  }

  static get user(): string {
    return process.env.USER || process.env.USERNAME || '';
  }

  // 检查是否在CI环境中运行
  static get isCI(): boolean {
    return !!(
      process.env.CI ||
      process.env.CONTINUOUS_INTEGRATION ||
      process.env.GITHUB_ACTIONS ||
      process.env.GITLAB_CI
    );
  }
}

4. Environment Variables

5. Shell命令处理

typescript
// env-utils.ts
export class EnvUtils {
  // Get environment variable with fallback
  static get(key: string, defaultValue?: string): string | undefined {
    return process.env[key] || defaultValue;
  }

  // Get PATH separator (: on Unix, ; on Windows)
  static get pathSeparator(): string {
    return process.platform === 'win32' ? ';' : ':';
  }

  // Split PATH into array
  static getPaths(): string[] {
    const pathVar = process.env.PATH || '';
    return pathVar.split(this.pathSeparator);
  }

  // Get common paths
  static get home(): string {
    return process.env.HOME || process.env.USERPROFILE || '';
  }

  static get user(): string {
    return process.env.USER || process.env.USERNAME || '';
  }

  // Check if running in CI
  static get isCI(): boolean {
    return !!(
      process.env.CI ||
      process.env.CONTINUOUS_INTEGRATION ||
      process.env.GITHUB_ACTIONS ||
      process.env.GITLAB_CI
    );
  }
}
typescript
// shell-utils.ts
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

export class ShellUtils {
  // 执行命令并处理平台差异
  static async execute(command: string): Promise<string> {
    try {
      const { stdout, stderr } = await execAsync(command, {
        shell: this.getShell()
      });
      if (stderr) console.error(stderr);
      return stdout.trim();
    } catch (error) {
      throw new Error(`Command failed: ${error.message}`);
    }
  }

  // 获取平台特定的Shell
  static getShell(): string {
    if (process.platform === 'win32') {
      return 'cmd.exe';
    }
    return process.env.SHELL || '/bin/sh';
  }

  // 平台特定命令
  static async listFiles(directory: string): Promise<string> {
    if (process.platform === 'win32') {
      return this.execute(`dir "${directory}"`);
    }
    return this.execute(`ls -la "${directory}"`);
  }

  static async clearScreen(): Promise<void> {
    if (process.platform === 'win32') {
      await this.execute('cls');
    } else {
      await this.execute('clear');
    }
  }

  static async openFile(filepath: string): Promise<void> {
    if (process.platform === 'win32') {
      await this.execute(`start "" "${filepath}"`);
    } else if (process.platform === 'darwin') {
      await this.execute(`open "${filepath}"`);
    } else {
      await this.execute(`xdg-open "${filepath}"`);
    }
  }
}

5. Shell Commands

6. 文件权限处理

typescript
// shell-utils.ts
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

export class ShellUtils {
  // Execute command with platform-specific handling
  static async execute(command: string): Promise<string> {
    try {
      const { stdout, stderr } = await execAsync(command, {
        shell: this.getShell()
      });
      if (stderr) console.error(stderr);
      return stdout.trim();
    } catch (error) {
      throw new Error(`Command failed: ${error.message}`);
    }
  }

  // Get platform-specific shell
  static getShell(): string {
    if (process.platform === 'win32') {
      return 'cmd.exe';
    }
    return process.env.SHELL || '/bin/sh';
  }

  // Platform-specific commands
  static async listFiles(directory: string): Promise<string> {
    if (process.platform === 'win32') {
      return this.execute(`dir "${directory}"`);
    }
    return this.execute(`ls -la "${directory}"`);
  }

  static async clearScreen(): Promise<void> {
    if (process.platform === 'win32') {
      await this.execute('cls');
    } else {
      await this.execute('clear');
    }
  }

  static async openFile(filepath: string): Promise<void> {
    if (process.platform === 'win32') {
      await this.execute(`start "" "${filepath}"`);
    } else if (process.platform === 'darwin') {
      await this.execute(`open "${filepath}"`);
    } else {
      await this.execute(`xdg-open "${filepath}"`);
    }
  }
}
typescript
// permissions.ts
import fs from 'fs';
import path from 'path';

export class FilePermissions {
  // 将文件设置为可执行(仅Unix系统)
  static makeExecutable(filepath: string): void {
    if (process.platform !== 'win32') {
      fs.chmodSync(filepath, 0o755);
    }
  }

  // 检查文件是否可执行
  static isExecutable(filepath: string): boolean {
    if (process.platform === 'win32') {
      // Windows系统检查文件扩展名
      const ext = path.extname(filepath).toLowerCase();
      return ['.exe', '.bat', '.cmd', '.com'].includes(ext);
    }

    try {
      fs.accessSync(filepath, fs.constants.X_OK);
      return true;
    } catch {
      return false;
    }
  }

  // 创建具有特定权限的文件(Unix系统)
  static createWithPermissions(
    filepath: string,
    content: string,
    mode: number = 0o644
  ): void {
    fs.writeFileSync(filepath, content, { mode });
  }
}

6. File Permissions

7. 进程管理

typescript
// permissions.ts
import fs from 'fs';
import path from 'path';

export class FilePermissions {
  // Make file executable (Unix only)
  static makeExecutable(filepath: string): void {
    if (process.platform !== 'win32') {
      fs.chmodSync(filepath, 0o755);
    }
  }

  // Check if file is executable
  static isExecutable(filepath: string): boolean {
    if (process.platform === 'win32') {
      // On Windows, check file extension
      const ext = path.extname(filepath).toLowerCase();
      return ['.exe', '.bat', '.cmd', '.com'].includes(ext);
    }

    try {
      fs.accessSync(filepath, fs.constants.X_OK);
      return true;
    } catch {
      return false;
    }
  }

  // Create file with specific permissions (Unix)
  static createWithPermissions(
    filepath: string,
    content: string,
    mode: number = 0o644
  ): void {
    fs.writeFileSync(filepath, content, { mode });
  }
}
typescript
// process-utils.ts
import { spawn, ChildProcess } from 'child_process';

export class ProcessUtils {
  // 根据PID终止进程,处理平台特定信号
  static kill(pid: number, signal?: string): void {
    if (process.platform === 'win32') {
      // Windows不支持信号,使用taskkill
      spawn('taskkill', ['/pid', pid.toString(), '/f', '/t']);
    } else {
      process.kill(pid, signal || 'SIGTERM');
    }
  }

  // 启动进程并处理平台差异
  static spawnCommand(
    command: string,
    args: string[] = []
  ): ChildProcess {
    if (process.platform === 'win32') {
      // Windows需要通过cmd.exe运行命令
      return spawn('cmd', ['/c', command, ...args], {
        stdio: 'inherit',
        shell: true
      });
    }

    return spawn(command, args, {
      stdio: 'inherit',
      shell: true
    });
  }

  // 根据名称查找进程
  static async findProcess(name: string): Promise<number[]> {
    if (process.platform === 'win32') {
      const { stdout } = await execAsync(`tasklist /FI "IMAGENAME eq ${name}"`);
      // 解析Windows tasklist输出
      const pids: number[] = [];
      const lines = stdout.split('\n');
      for (const line of lines) {
        const match = line.match(/\s+(\d+)\s+/);
        if (match) pids.push(parseInt(match[1]));
      }
      return pids;
    } else {
      const { stdout } = await execAsync(`pgrep ${name}`);
      return stdout.split('\n').filter(Boolean).map(Number);
    }
  }
}

7. Process Management

8. 平台特定依赖项

typescript
// process-utils.ts
import { spawn, ChildProcess } from 'child_process';

export class ProcessUtils {
  // Kill process by PID with platform-specific signal
  static kill(pid: number, signal?: string): void {
    if (process.platform === 'win32') {
      // Windows doesn't support signals, use taskkill
      spawn('taskkill', ['/pid', pid.toString(), '/f', '/t']);
    } else {
      process.kill(pid, signal || 'SIGTERM');
    }
  }

  // Spawn process with platform-specific handling
  static spawnCommand(
    command: string,
    args: string[] = []
  ): ChildProcess {
    if (process.platform === 'win32') {
      // Windows requires cmd.exe to run commands
      return spawn('cmd', ['/c', command, ...args], {
        stdio: 'inherit',
        shell: true
      });
    }

    return spawn(command, args, {
      stdio: 'inherit',
      shell: true
    });
  }

  // Find process by name
  static async findProcess(name: string): Promise<number[]> {
    if (process.platform === 'win32') {
      const { stdout } = await execAsync(`tasklist /FI "IMAGENAME eq ${name}"`);
      // Parse Windows tasklist output
      const pids: number[] = [];
      const lines = stdout.split('\n');
      for (const line of lines) {
        const match = line.match(/\s+(\d+)\s+/);
        if (match) pids.push(parseInt(match[1]));
      }
      return pids;
    } else {
      const { stdout } = await execAsync(`pgrep ${name}`);
      return stdout.split('\n').filter(Boolean).map(Number);
    }
  }
}
json
// package.json
{
  "name": "my-app",
  "dependencies": {
    "common-dep": "^1.0.0"
  },
  "optionalDependencies": {
    "fsevents": "^2.3.2"
  },
  "devDependencies": {
    "@types/node": "^18.0.0"
  }
}
typescript
// platform-specific-module.ts
export async function loadPlatformModule() {
  if (process.platform === 'win32') {
    return await import('./windows/module');
  } else if (process.platform === 'darwin') {
    return await import('./macos/module');
  } else {
    return await import('./linux/module');
  }
}

// 为可选依赖项提供优雅降级
export function useFSEvents() {
  try {
    // fsevents仅适用于macOS
    if (process.platform === 'darwin') {
      const fsevents = require('fsevents');
      return fsevents;
    }
  } catch (error) {
    console.warn('fsevents不可用,使用降级方案');
  }

  // 降级为chokidar或fs.watch
  return require('chokidar');
}

8. Platform-Specific Dependencies

9. 跨平台测试

GitHub Actions矩阵配置

json
// package.json
{
  "name": "my-app",
  "dependencies": {
    "common-dep": "^1.0.0"
  },
  "optionalDependencies": {
    "fsevents": "^2.3.2"
  },
  "devDependencies": {
    "@types/node": "^18.0.0"
  }
}
typescript
// platform-specific-module.ts
export async function loadPlatformModule() {
  if (process.platform === 'win32') {
    return await import('./windows/module');
  } else if (process.platform === 'darwin') {
    return await import('./macos/module');
  } else {
    return await import('./linux/module');
  }
}

// Graceful fallback for optional dependencies
export function useFSEvents() {
  try {
    // fsevents is macOS only
    if (process.platform === 'darwin') {
      const fsevents = require('fsevents');
      return fsevents;
    }
  } catch (error) {
    console.warn('fsevents not available, using fallback');
  }

  // Fallback to chokidar or fs.watch
  return require('chokidar');
}
yaml
undefined

9. Testing Across Platforms

.github/workflows/test.yml

GitHub Actions Matrix

yaml
undefined
name: Cross-Platform Tests
on: [push, pull_request]
jobs: test: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] node-version: [16, 18, 20]
runs-on: ${{ matrix.os }}

steps:
  - uses: actions/checkout@v3

  - name: Setup Node.js
    uses: actions/setup-node@v3
    with:
      node-version: ${{ matrix.node-version }}

  - name: Install dependencies
    run: npm ci

  - name: Run tests
    run: npm test

  - name: Platform-specific tests
    if: runner.os == 'Windows'
    run: npm run test:windows

  - name: Platform-specific tests
    if: runner.os == 'macOS'
    run: npm run test:macos

  - name: Platform-specific tests
    if: runner.os == 'Linux'
    run: npm run test:linux
undefined

.github/workflows/test.yml

平台特定测试用例

name: Cross-Platform Tests
on: [push, pull_request]
jobs: test: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] node-version: [16, 18, 20]
runs-on: ${{ matrix.os }}

steps:
  - uses: actions/checkout@v3

  - name: Setup Node.js
    uses: actions/setup-node@v3
    with:
      node-version: ${{ matrix.node-version }}

  - name: Install dependencies
    run: npm ci

  - name: Run tests
    run: npm test

  - name: Platform-specific tests
    if: runner.os == 'Windows'
    run: npm run test:windows

  - name: Platform-specific tests
    if: runner.os == 'macOS'
    run: npm run test:macos

  - name: Platform-specific tests
    if: runner.os == 'Linux'
    run: npm run test:linux
undefined
typescript
// tests/platform.test.ts
import { Platform } from '../src/platform-utils';

describe('Platform-specific tests', () => {
  describe('File paths', () => {
    it('should handle paths correctly', () => {
      const configPath = path.join(os.homedir(), 'config.json');

      if (Platform.isWindows) {
        expect(configPath).toMatch(/^[A-Z]:\\/);
      } else {
        expect(configPath).toMatch(/^\//);
      }
    });
  });

  describe.skipIf(Platform.isWindows)('Unix-only tests', () => {
    it('should work with symlinks', () => {
      // 符号链接测试
    });

    it('should handle file permissions', () => {
      // 权限测试
    });
  });

  describe.skipIf(!Platform.isWindows)('Windows-only tests', () => {
    it('should work with UNC paths', () => {
      // UNC路径测试
    });

    it('should handle drive letters', () => {
      // 盘符测试
    });
  });
});

Platform-Specific Tests

10. 字符编码处理

typescript
// tests/platform.test.ts
import { Platform } from '../src/platform-utils';

describe('Platform-specific tests', () => {
  describe('File paths', () => {
    it('should handle paths correctly', () => {
      const configPath = path.join(os.homedir(), 'config.json');

      if (Platform.isWindows) {
        expect(configPath).toMatch(/^[A-Z]:\\/);
      } else {
        expect(configPath).toMatch(/^\//);
      }
    });
  });

  describe.skipIf(Platform.isWindows)('Unix-only tests', () => {
    it('should work with symlinks', () => {
      // Symlink tests
    });

    it('should handle file permissions', () => {
      // Permission tests
    });
  });

  describe.skipIf(!Platform.isWindows)('Windows-only tests', () => {
    it('should work with UNC paths', () => {
      // UNC path tests
    });

    it('should handle drive letters', () => {
      // Drive letter tests
    });
  });
});
typescript
// encoding-utils.ts
import iconv from 'iconv-lite';

export class EncodingUtils {
  // 读取指定编码的文件
  static readFile(filepath: string, encoding: string = 'utf8'): string {
    const buffer = fs.readFileSync(filepath);

    if (encoding === 'utf8') {
      // 移除可能存在的BOM
      if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
        return buffer.slice(3).toString('utf8');
      }
      return buffer.toString('utf8');
    }

    return iconv.decode(buffer, encoding);
  }

  // 写入指定编码的文件
  static writeFile(
    filepath: string,
    content: string,
    encoding: string = 'utf8'
  ): void {
    if (encoding === 'utf8') {
      fs.writeFileSync(filepath, content, 'utf8');
    } else {
      const buffer = iconv.encode(content, encoding);
      fs.writeFileSync(filepath, buffer);
    }
  }

  // 检测文件编码
  static detectEncoding(filepath: string): string {
    const buffer = fs.readFileSync(filepath);

    // 检查BOM
    if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
      return 'utf8';
    }
    if (buffer[0] === 0xFE && buffer[1] === 0xFF) {
      return 'utf16be';
    }
    if (buffer[0] === 0xFF && buffer[1] === 0xFE) {
      return 'utf16le';
    }

    // 默认UTF-8
    return 'utf8';
  }
}

10. Character Encoding

11. 构建配置

typescript
// encoding-utils.ts
import iconv from 'iconv-lite';

export class EncodingUtils {
  // Read file with specific encoding
  static readFile(filepath: string, encoding: string = 'utf8'): string {
    const buffer = fs.readFileSync(filepath);

    if (encoding === 'utf8') {
      // Remove BOM if present
      if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
        return buffer.slice(3).toString('utf8');
      }
      return buffer.toString('utf8');
    }

    return iconv.decode(buffer, encoding);
  }

  // Write file with specific encoding
  static writeFile(
    filepath: string,
    content: string,
    encoding: string = 'utf8'
  ): void {
    if (encoding === 'utf8') {
      fs.writeFileSync(filepath, content, 'utf8');
    } else {
      const buffer = iconv.encode(content, encoding);
      fs.writeFileSync(filepath, buffer);
    }
  }

  // Detect encoding
  static detectEncoding(filepath: string): string {
    const buffer = fs.readFileSync(filepath);

    // Check for BOM
    if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
      return 'utf8';
    }
    if (buffer[0] === 0xFE && buffer[1] === 0xFF) {
      return 'utf16be';
    }
    if (buffer[0] === 0xFF && buffer[1] === 0xFE) {
      return 'utf16le';
    }

    // Default to UTF-8
    return 'utf8';
  }
}
typescript
// rollup.config.js
export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/index.js',
      format: 'cjs'
    },
    {
      file: 'dist/index.esm.js',
      format: 'esm'
    }
  ],
  external: [
    // 将平台特定模块标记为外部依赖
    'fsevents'
  ],
  plugins: [
    // 构建时替换平台检测代码,优化Tree-Shaking
    replace({
      'process.platform': JSON.stringify(process.platform),
      preventAssignment: true
    })
  ]
};

11. Build Configuration

最佳实践

✅ 推荐做法

typescript
// rollup.config.js
export default {
  input: 'src/index.ts',
  output: [
    {
      file: 'dist/index.js',
      format: 'cjs'
    },
    {
      file: 'dist/index.esm.js',
      format: 'esm'
    }
  ],
  external: [
    // Mark platform-specific modules as external
    'fsevents'
  ],
  plugins: [
    // Replace platform checks at build time for better tree-shaking
    replace({
      'process.platform': JSON.stringify(process.platform),
      preventAssignment: true
    })
  ]
};
  • 使用path.join()或path.resolve()处理路径
  • 使用os.EOL处理行尾符
  • 必要时在运行时检测平台
  • 在所有目标平台上进行测试
  • 对平台特定模块使用optionalDependencies
  • 优雅处理文件权限
  • 对用户输入进行Shell转义
  • 规范化文本文件的行尾符
  • 默认使用UTF-8编码
  • 记录平台特定行为
  • 为平台特定功能提供降级方案
  • 使用CI/CD在多平台上测试

Best Practices

❌ 不推荐做法

✅ DO

  • Use path.join() or path.resolve() for paths
  • Use os.EOL for line endings
  • Detect platform at runtime when needed
  • Test on all target platforms
  • Use optionalDependencies for platform-specific modules
  • Handle file permissions gracefully
  • Use shell escaping for user input
  • Normalize line endings in text files
  • Use UTF-8 encoding by default
  • Document platform-specific behavior
  • Provide fallbacks for platform-specific features
  • Use CI/CD to test on multiple platforms
  • 硬编码带有反斜杠或正斜杠的文件路径
  • 假设仅支持Unix特性(信号、权限、符号链接)
  • 忽略Windows特定特性(盘符、UNC路径)
  • 使用平台特定命令而不提供降级方案
  • 假设文件系统区分大小写
  • 忽略不同的行尾符差异
  • 未检查就使用平台特定API
  • 硬编码环境变量访问方式
  • 忽略字符编码问题

❌ DON'T

常见模式

模式1:平台工厂模式

  • Hardcode file paths with backslashes or forward slashes
  • Assume Unix-only features (signals, permissions, symlinks)
  • Ignore Windows-specific quirks (drive letters, UNC paths)
  • Use platform-specific commands without fallbacks
  • Assume case-sensitive file systems
  • Forget about different line endings
  • Use platform-specific APIs without checking
  • Hardcode environment variable access patterns
  • Ignore character encoding issues
typescript
export interface PlatformHandler {
  openFile(path: string): Promise<void>;
  getConfigPath(): string;
}

class WindowsHandler implements PlatformHandler {
  async openFile(path: string) {
    await exec(`start "" "${path}"`);
  }
  getConfigPath() {
    return path.join(process.env.APPDATA!, 'myapp', 'config.json');
  }
}

class UnixHandler implements PlatformHandler {
  async openFile(path: string) {
    await exec(`xdg-open "${path}"`);
  }
  getConfigPath() {
    return path.join(os.homedir(), '.config', 'myapp', 'config.json');
  }
}

export function createPlatformHandler(): PlatformHandler {
  return process.platform === 'win32'
    ? new WindowsHandler()
    : new UnixHandler();
}

Common Patterns

模式2:条件导入

Pattern 1: Platform Factory

typescript
export interface PlatformHandler {
  openFile(path: string): Promise<void>;
  getConfigPath(): string;
}

class WindowsHandler implements PlatformHandler {
  async openFile(path: string) {
    await exec(`start "" "${path}"`);
  }
  getConfigPath() {
    return path.join(process.env.APPDATA!, 'myapp', 'config.json');
  }
}

class UnixHandler implements PlatformHandler {
  async openFile(path: string) {
    await exec(`xdg-open "${path}"`);
  }
  getConfigPath() {
    return path.join(os.homedir(), '.config', 'myapp', 'config.json');
  }
}

export function createPlatformHandler(): PlatformHandler {
  return process.platform === 'win32'
    ? new WindowsHandler()
    : new UnixHandler();
}
typescript
const platformModule = await (async () => {
  switch (process.platform) {
    case 'win32':
      return import('./platforms/windows');
    case 'darwin':
      return import('./platforms/macos');
    default:
      return import('./platforms/linux');
  }
})();

Pattern 2: Conditional Imports

工具与资源

typescript
const platformModule = await (async () => {
  switch (process.platform) {
    case 'win32':
      return import('./platforms/windows');
    case 'darwin':
      return import('./platforms/macos');
    default:
      return import('./platforms/linux');
  }
})();
  • cross-env: 跨平台设置环境变量
  • cross-spawn: 跨平台进程启动工具
  • rimraf: 跨平台rm -rf替代工具
  • mkdirp: 跨平台mkdir -p替代工具
  • cpy: 跨平台文件复制工具
  • del: 跨平台文件删除工具
  • execa: 增强版child_process工具
  • pkg: 将Node.js应用打包为多平台可执行文件

Tools & Resources

  • cross-env: Set environment variables cross-platform
  • cross-spawn: Cross-platform spawn
  • rimraf: Cross-platform rm -rf
  • mkdirp: Cross-platform mkdir -p
  • cpy: Cross-platform file copying
  • del: Cross-platform file deletion
  • execa: Better child_process
  • pkg: Package Node.js apps for all platforms