| name | cross-platform-compatibility |
| description | Handle cross-platform compatibility including file paths, environment detection, platform-specific dependencies, and testing across Windows, macOS, and Linux. Use when dealing with platform-specific code or OS compatibility. |
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.
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
Instructions
1. File Path Handling
Node.js Path Module
const configPath = 'C:\\Users\\user\\config.json';
const dataPath = '/home/user/data.txt';
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');
const basename = path.basename('/path/to/file.txt');
const extname = path.extname('/path/to/file.txt');
const normalized = path.normalize('/path/to/../file.txt');
Python Path Handling
config_path = 'C:\\Users\\user\\config.json'
data_path = '/home/user/data.txt'
from pathlib import Path
import os
config_path = Path.home() / 'config' / 'app.json'
data_path = Path.cwd() / 'data' / 'users.txt'
if config_path.exists():
content = config_path.read_text()
dirname = config_path.parent
filename = config_path.name
extension = config_path.suffix
absolute_path = Path('./config/settings.json').resolve()
output_dir = Path('output')
output_dir.mkdir(parents=True, exist_ok=True)
Go Path Handling
package main
import (
"os"
"path/filepath"
)
func main() {
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
Node.js Platform Detection
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;
},
get homeDir(): string {
return os.homedir();
},
(): {
os.();
}
};
(.) {
.();
} (.) {
.();
} (.) {
.();
}
(. === ) {
.();
}
Python Platform Detection
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()
@staticmethod
def version():
return platform.version()
Platform.is_windows():
()
Platform.is_macos():
()
Platform.is_linux():
()
3. Line Endings
import os from 'os';
export const LineEnding = {
LF: '\n',
CRLF: '\r\n',
CR: '\r',
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);
}
};
fileContent = fs.(, );
normalized = .(fileContent);
unixContent = .(fileContent);
fs.(, normalized);
4. Environment Variables
export class EnvUtils {
static get(key: string, defaultValue?: string): string | undefined {
return process.env[key] || defaultValue;
}
static get pathSeparator(): string {
return process.platform === 'win32' ? ';' : ':';
}
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(): {
process.. || process.. || ;
}
(): {
!!(
process.. ||
process.. ||
process.. ||
process..
);
}
}
5. Shell Commands
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}`);
}
}
static getShell(): string {
if (process.platform === 'win32') {
return 'cmd.exe';
}
return process.env.SHELL || ;
}
(: ): <> {
(process. === ) {
.();
}
.();
}
(): <> {
(process. === ) {
.();
} {
.();
}
}
(: ): <> {
(process. === ) {
.();
} (process. === ) {
.();
} {
.();
}
}
}
6. File Permissions
import fs from 'fs';
import path from 'path';
export class FilePermissions {
static makeExecutable(filepath: string): void {
if (process.platform !== 'win32') {
fs.chmodSync(filepath, 0o755);
}
}
static isExecutable(filepath: string): boolean {
if (process.platform === 'win32') {
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;
}
}
(
: ,
: ,
: =
): {
fs.(filepath, content, { mode });
}
}
7. Process Management
import { spawn, ChildProcess } from 'child_process';
export class ProcessUtils {
static kill(pid: number, signal?: string): void {
if (process.platform === 'win32') {
spawn('taskkill', ['/pid', pid.toString(), '/f', '/t']);
} else {
process.kill(pid, signal || 'SIGTERM');
}
}
static spawnCommand(
command: string,
args: string[] = []
): ChildProcess {
if (process.platform === 'win32') {
return spawn('cmd', ['/c', command, ...args], {
stdio: 'inherit',
shell: true
});
}
(command, args, {
: ,
:
});
}
(: ): <[]> {
(process. === ) {
{ stdout } = ();
: [] = [];
lines = stdout.();
( line lines) {
match = line.();
(match) pids.((match[]));
}
pids;
} {
{ stdout } = ();
stdout.().().();
}
}
}
8. Platform-Specific Dependencies
{
"name": "my-app",
"dependencies": {
"common-dep": "^1.0.0"
},
"optionalDependencies": {
"fsevents": "^2.3.2"
},
"devDependencies": {
"@types/node": "^18.0.0"
}
}
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 {
if (process.platform === 'darwin') {
const fsevents = require('fsevents');
return fsevents;
}
} catch (error) {
console.warn('fsevents not available, using fallback');
}
return require('chokidar');
}
9. Testing Across Platforms
GitHub Actions Matrix
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
Platform-Specific Tests
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', {
});
(, {
});
});
});
10. Character Encoding
import iconv from 'iconv-lite';
export class EncodingUtils {
static readFile(filepath: string, encoding: string = 'utf8'): string {
const buffer = fs.readFileSync(filepath);
if (encoding === 'utf8') {
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.(filepath, content, );
} {
buffer = iconv.(content, encoding);
fs.(filepath, buffer);
}
}
(: ): {
buffer = fs.(filepath);
(buffer[] === && buffer[] === && buffer[] === ) {
;
}
(buffer[] === && buffer[] === ) {
;
}
(buffer[] === && buffer[] === ) {
;
}
;
}
}
11. Build Configuration
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.js',
format: 'cjs'
},
{
file: 'dist/index.esm.js',
format: 'esm'
}
],
external: [
'fsevents'
],
plugins: [
replace({
'process.platform': JSON.stringify(process.platform),
preventAssignment: true
})
]
};
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
❌ DON'T
- 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
Common Patterns
Pattern 1: Platform Factory
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 (): {
process. ===
? ()
: ();
}
Pattern 2: Conditional Imports
const platformModule = await (async () => {
switch (process.platform) {
case 'win32':
return import('./platforms/windows');
case 'darwin':
return import('./platforms/macos');
default:
return import('./platforms/linux');
}
})();
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