ehmicky / portable-node-guide
- вторник, 12 марта 2019 г. в 00:16:49
📗 Practical guide on how to write portable/cross-platform Node.js code
Practical guide on how to write portable/cross-platform Node.js code.
According to the 2018 Node.js user survey (using the raw data), 24% of Node.js developers use Windows locally and 41% use Mac. In production 85% use Linux and 1% use BSD.
If you find this document too long, you can jump to the summary.
If you want to keep up to date on portability issues introduced with each new Node.js release, follow @ehmicky on Twitter.
Did you find an error or want to add more information? Issues and PRs are welcome!
Installers for each major OS are available on the Node.js website.
To install, switch and update Node.js versions
nvm can be used on Linux/Mac. It
does not support Windows
but nvm-windows and
nvs are alternatives that do.
To upgrade npm on Windows, it is convenient to use
npm-windows-upgrade.
Each OS has its own set of (from the lowest to the highest level):
fork.sed.vim or
Notepad.Directly executing one of those binaries (e.g. calling sed) won't usually
work on every OS.
There are several approaches to solve this:
child_process methods are
executing OS-specific system calls under the hood.MinGW for
gcc on Windows.msys for
Bash on Windows.
Shipped with Git for Windows.shelljsnode-windowsopn abstract
common user applications.Few lower-level tools attempt to bring cross-platform compatibility by emulating or translating system calls:
Any OS can be run locally using virtual machines. Windows provides with official images.
It is recommended to run automated tests on a continuous integration provider that supports Linux, Mac and Windows, which most high-profile providers now do.
Windows users must first run
npm install -g windows-build-tools
as an admin before being able to install
C/C++ addons.
Typical directory locations are OS-specific:
/tmp on Linux,
/var/folders/.../T on Mac or C:\Users\USER\AppData\Local\Temp on
Windows. os.tmpdir() can be
used to retrieve it on any OS./home/USER on Linux,
/Users/USER on Mac or C:\Users\USER on Windows.
os.homedir() can be used
to retrieve it on any OS.Man pages are Unix-specific so the
package.json's man field
does not have any effects on Windows.
While Unix usually stores system configuration as files, Windows uses the registry, a central key-value database. Some projects like node-winreg, rage-edit or windows-registry-node can be used to access it from Node.
This should only be done when accessing OS-specific settings. Otherwise storing configuration as files or remotely is easier and more portable.
The character encoding on Unix is usually UTF-8. However on Windows it is usually either UTF-16 or one of the Windows code pages. Few non-Unicode character encodings are also popular in some countries. This can result in characters not being printed properly, especially high Unicode code points and emoji.
The character encoding can be specified using an encoding option with most
relevant Node.js core methods.
UTF-8 is always the default value except
for
readable streams
(including
fs.createReadStream()),
fs.readFile() and
most crypto methods where buffer is
the default instead.
To convert between character encodings
string_encoder (decoding only),
Buffer.transcode(),
TextDecoder
and
TextEncoder
can be used.
Node.js
supports
UTF-8,
UTF-16 little endian,
Latin-1 and
ASCII, except for
TextDecoder
and
TextEncoder
which support
UTF-8,
UTF-16 little endian and
UTF-16 big endian by default. If
Node.js is built with
full internationalization support
or provided with it at runtime,
many more character encodings
are supported by
TextDecoder
and
TextEncoder.
If doing so is inconvenient,
iconv-lite or
iconv can be used instead.
It is recommended to always use UTF-8. When reading from a file or terminal, one should either:
node-chardet or
jschardet and convert to
UTF-8.When writing to a terminal the character encoding will almost always be
UTF-8 on Unix and
CP866 on Windows (cmd.exe).
figures and
log-symbols can be used to
print common symbols consistently across platforms.
The character representation of a
newline is OS-specific. On Unix it
is \n (line feed) while on Windows it is \r\n (carriage return followed by
line feed).
Newlines inside a template string translate to \n on any OS.
const string = `this is
an example`Some Windows applications, including the cmd.exe terminal, print \n as
newlines, so using \n will work just fine. However some Windows applications
don't, which is why when reading from or writing to a file the OS-specific
newline os.EOL should be used
instead of \n.
The substitute character
(CTRL-Z)
stops file streams
on some Windows commands when in text mode. This includes the
type command in cmd.exe. As a consequence
that character should be avoided in non-binary files.
As opposed to Windows, Unix does not implicitely add a newline at the end of files. Thus it is recommended to end files with a newline character. However please remember that Windows will print these as if two newlines were present instead.
The BOM is a special character at the beginning of a file indicating its endianness and character encoding. Since it creates issues with shebangs and adds little value to UTF-8, it is better not to add it to new files. However if a BOM is present in input, it should be properly handled. Fortunately this is the default behavior of Node.js core methods, so this should usually not create any issues.
Character encoding, newlines and EOF behavior should be specified with editorconfig. They can also be enforced with tools like ESLint and Prettier.
While / is used as a file path delimiter on Unix (/file/to/path), \ is
used on Windows instead (\file\to\path). The path delimiter can be retrieved
with path.sep. Windows
actually allows using or mixing in / delimiters in file paths most of the
time, but not always so this should not be relied on.
Furthermore absolute paths always start with / on Unix, but on Windows they
can take
many shapes:
\: the current drive.C:\: a specific drive (here C:). This can also be used with relative
paths like C:file\to\path.\\HOST\: UNC path, for remote hosts.\\?\: allows to overcome file path length limit of 260 characters.
Those can be produced in Node.js with
path.toNamespacedPath().\\.\: device path.When file paths are used as arguments to Node.js core methods:
require(path),
fs.*(path) methods,
path.*() methods or
process.chdir(path).When file paths are returned by Node.js core methods:
path.*() methods,
process.cwd(),
os.homedir(),
os.tmpdir()
or the value of
__dirname,
process.argv
and process.execPath.path.win32.*() or
path.posix.*()
instead of path.*() will return
Windows or Unix paths.fs.createReadStream()
and
fs.mkdtemp().Outside of Node.js, i.e. when the path is input from (or output to) the terminal or a file, its syntax is OS-specific.
To summarize:
path.normalize()
should be used to make it OS-specific.Each OS tends to use its own file system: Windows uses NTFS, Mac uses APFS (previously HFS+) and Linux tends to use ext4, Btrfs or XFS. Each file system has its own restrictions when it comes to naming files and paths.
Portable filenames need to avoid:
a-z, 0-9, -._,=()~-.com1, com2, com3, com4, com5, com6, com7,
com8, com9, lpt1, lpt2, lpt3, lpt4, lpt5,
lpt6, lpt7, lpt8, lpt9, con, nul, prn, aux.Portable file paths need to avoid:
npm deeply nesting node_modules but not anymore with the latest
npm versions.~ or ~user
home directory shorthand.Unix usually comes with Bash but not always. Popular alternatives include Fish, Dash, tcsh, ksh and zsh.
Writing interoperable shell code can be somewhat achieved by using either:
However this won't work on Windows which uses two other shells:
cmd.exe
which comes by default.cmd.exe is very different from Bash and has quite many limitations:
; cannot be used to separate statements. However && can be used like
in Bash./opt) instead of dashes (-opt). But
Node.js binaries can still use -opt.*) does not work.%errorlevel% instead of $?.^. This is partially solved with the
child_process.spawn()
option windowsVerbatimArguments which defaults to true when cmd.exe is
used.When the option shell of
child_process.spawn()
is true, /bin/sh will be used on Unix and cmd.exe (or the environment
variable ComSpec) will be used on Windows. Since those shells behave
differently it is better to avoid that option.
As a consequence it is recommended to:
command arguments... callsexeca() (not execa.shell())
to fire those.Shebang like #!/usr/bin/node
do not work on Windows, where only files ending with .exe, .com, .cmd
or .bat can be directly executed. Portable file execution must either:
node file.js instead of ./file.js.cross-spawn
(which is included in execa).During file execution the extension can be omitted on Windows if it is listed
in the PATHEXT environment
variable, which defaults to
.COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC. This won't work on
Unix.
The PATH environment
variable uses ; instead of : as delimiter on Windows. This can be retrieved
with
path.delimiter.
When the option
detached: false
of
child_process.spawn()
is used, the child process will be terminated when its parent is on Windows, but not on Unix.
When the option
detached: true
is used instead, a new terminal window will appear on Windows unless the option
windowsHide: true
is used (requires Node >= 8.8.0).
Finally the option
argv0
does not modify process.title on Windows.
Many of those differences can be solved by using
execa.
Package binaries
(package.json's bin field)
are installed in the node_modules/.bin folder by npm install.
On Unix those are symlinks pointing to the executable files. They can be executed directly inside a terminal.
On Windows, each package binary creates instead two files for the same purpose:
.cmd which can be executed directly
inside cmd.exe.sh or
bash.The syntax to
reference environment variables is
$VARIABLE on Unix but %VARIABLE% on Windows. Also if the variable is
missing, its value will be '' on Unix but '%VARIABLE%' on Windows.
To pass
environment variables
to a command, it must be prepended with VARIABLE=value ... on Unix. However on
Windows one must use Set VARIABLE=value or setx VARIABLE value as separate
statements. cross-env can be used
to both reference and pass environment variables on any OS.
To list the current
environment variables
env must be used on Unix and set on Windows. However
process.env will
work on any OS.
Environment variables are case insensitive on Windows but not on Unix.
path-key can be used to solve this
for the PATH environment variable.
Finally most environment variables names are OS-specific:
SHELL on Unix is ComSpec on Windows. Unfortunately
os.userInfo().shell
returns null on Windows.PS1 on Unix is PROMPT on Windows.PWD on Unix is CD on Windows.
process.cwd()
and
process.chdir()
should be used instead.HOME on Unix is USERPROFILE on Windows.
os.homedir() (faster)
os.userInfo().homedir
(more accurate) should be used instead.TMPDIR in Unix is TMP or TEMP on Windows.
os.tmpdir() should be used
instead.USER or LOGNAME on Unix is USERDOMAIN and USERNAME on Windows.
username or
os.userInfo().username
should be used instead.HOSTNAME on Unix is COMPUTERNAME on Windows.
os.hostname() should be
used instead.The project osenv can be used to retrieve
OS-specific environment variables names.
Windows (but not Unix) can use
junctions.
fs.symlink()
allows creating these.
Creating regular symlinks on Windows will most likely fail because it requires a "create symlink" permission which by default is off for non-admins. Also some file systems like FAT do not allow symlinks. As a consequence it is more portable to copy files instead of symlinking them.
Neither junctions nor hard links
(fs.link())
require permissions on Windows.
The blksize and
blocks values of
fs.stat()
are undefined on Windows. On the other hand the
birthtime and
birthtimeMs values are
undefined on Unix.
The O_NOATIME flag
of
fs.open()
only works on Linux.
fs.watch() is not very portable.
For example the option recursive does not work on Linux.
chokidar can be used instead.
Unix uses POSIX permissions but Windows is based on a combination of:
readonly, hidden and system.
winattr and
hidefile can be used to
manipulate those.Node.js does not support Windows permissions.
fs.chmod(),
fs.stat()'s
mode,
fs.access(),
fs.open()'s
mode,
fs.mkdir()'s
options.mode and
process.umask() only work on
Unix with some minor exceptions:
fs.access()
F_OK works.fs.access()
W_OK checks
the readonly file attribute on Windows. This is quite limited as it does
not check other file attributes nor ACLs.readonly file attribute is checked on Windows when the write POSIX
permission is missing for any user class (user, group or others).On the other hand
fs.open()
works correctly on Windows where
flags are being
translated to Windows-specific file attributes and permissions.
Another difference on Windows: to execute files their extension must be listed
in the environment variable
PATHEXT.
Finally
fs.lchmod()
is only available on Mac.
Unix users are identified with a
UID and a
GID while Windows users
are identified with a
SID.
Consequently all methods based on
UID or
GID fail on Windows:
os.userInfo().uid|gid return -1.fs.stat()'s
uid and
gid return 0.process methods getuid(),
geteuid(),
getgid(),
getegid(),
setuid(),
seteuid(),
setgid(),
setegid(),
getgroups(),
setgroups() and
initgroups()
throw an error.fs.chown()
does not do anything.The privileged user is root on Unix and admin on Windows. Those are
triggered with different mechanisms. One can use
is-elevated (and the related
is-admin and
is-root) to check it on any OS.
The resolution of
process.hrtime()
is hardware-specific and varies between 1 nanosecond and 1 millisecond.
The main way to identify the current OS is to use
process.platform
(or the identical
os.platform()).
The os core module offers some
finer-grained identification methods but those are rarely needed:
os.type() is similar but
slighly more precise.os.release() returns the
OS version number, e.g. 3.11.0-14-generic (Linux), 18.0.0 (Mac) or
10.0.17763 (Windows).os.arch() (or the identical
process.arch)
returns the CPU architecture, e.g. arm or x64.os.endianness()
returns the CPU endianness, i.e. BE or LE.Some projects allow retrieving:
getos: the Linux distribution
name.osname (and the related
windows-release and
macos-release): the OS
name and version in a human-friendly way.is-windows: whether current
OS is Windows, including through MSYS and
Cygwin.is-wsl: whether current OS is
Windows though
WSL.Uptime, memory and CPUs can be retrieved on any OS using
os.uptime(),
process.uptime(),
os.freemem(),
os.totalmem(),
process.memoryUsage(),
os.cpus() and
process.cpuUsage().
However:
os.cpus()'s times.nice is
0 on Windows.os.loadavg() is an array
of 0 on Windows.systeminformation can
be used for more device information.
os.networkInterfaces()
and os.hostname()
work on any OS.
However on Windows:
\\.\pipe\listen()
on a file descriptor.cluster.schedulingPolicy
SCHED_RR is inefficient, so the default value is SCHED_NONE.process.pid,
process.ppid,
process.title,
os.getPriority() and
os.setPriority()
work on any OS.
Other projects can be used to manipulate processes:
ps-list: list processes.
tasklist and
fastlist can also be used
for Windows only.pid-from-port: find processes
by port.process-exists: check
if a process is running.Windows do not use signals like Unix does.
However processes can be terminated using the
taskkill
command. The taskkill project can
be used to do it from Node.js. fkill
builds on it to terminate processes on any OS.
Which signals can be used is OS-specific:
process.kill()
and
process.on(signal)
can
only use the following signals on Windows:
SIGINT, SIGTERM, SIGKILL and 0.process.on(signal)
(but not
process.kill())
can be used on Windows with:
SIGABRTSIGHUP: closing cmd.exeSIGBREAK: CTRL-BREAK on cmd.exeSIGWINCH: resizing the terminal. This will only
be triggered
on Windows when the cursor moves on when a terminal in raw mode is used.SIGILL, SIGFPE and SIGSEGV but listening to those signals is
not recommendedSIGPOLL, SIGPWR and SIGUNUSED can only be used on Linux.SIGINFO can only be used on Mac.Each signal has both an OS-agnostic name and an OS-specific integer constant.
process.kill()
can use either. It is possible to convert between both using
os.constants.signals.
However it is more portable to use signal names instead of integer constants.
--diagnostic-report-on-signal
does not work on Windows.
Node errors can be identified with either:
error.code: an
OS-agnostic string (more portable).error.errno: an
OS-specific integer constant.It is possible to convert between both using
os.constants.errno and
util.getSystemErrorName.
Most available error.code
start with E and
can be fired on any OS. However few
start with W
and can only be fired on Windows.
Some anti-virus software on Windows
have been reported to lock
directories and make fs.rename() fail.
graceful-fs or
rimraf solves this by retrying few
milliseconds later.
nvm use
nvm-windows and
npm-windows-upgrade
on Windows.npm install -g windows-build-tools
on Windows when installing C/C++ addons.os Node.js core module when needed.UTF-8. File/terminal input
should either be validated or converted to it
(node-chardet).os.EOL when reading from or
writing to a file, \n otherwise.CTRL-Z) in non-binary files.path.normalize()
when writing a file path to a terminal or file. Otherwise use Unix paths
(slashes).a-z, 0-9 and -._,=() in filenames.execa.command arguments... calls, optionally
chained with &&.cross-env.chokidar to watch files.blksize,
blocks,
mode,
uid,
gid,
birthtime and
birthtimeMs returned
by
fs.stat().fs.chmod(),
fs.access()
(except F_OK),
fs.open()'s
mode,
fs.mkdir()'s
options.mode and
process.umask().os.userInfo().uid|gid,
fs.chown()
and the process methods
getuid(),
geteuid(),
getgid(),
getegid(),
setuid(),
seteuid(),
setgid(),
setegid(),
getgroups(),
setgroups() and
initgroups().process.hrtime()
is nanoseconds-precise.process.platform.os.cpus() times.nice and
os.loadavg().systeminformation
to retrieve any device information not available through the
os core module.\\.\pipe\ on Windows.listen()
on a file descriptor.ps-list,
pid-from-port and
process-exists to find
and check for processes.fkill to terminate processes.process.kill()
with the following signals: SIGINT, SIGTERM, SIGKILL and 0.process.on(signal)
with the following signals: SIGINT, SIGTERM, SIGKILL, 0, SIGWINCH,
SIGABRT, SIGHUP and SIGBREAK.--diagnostic-report-on-signalerror.code
over error.errno.Did you find an error or want to add more information? Did you have problems understanding a specific section? Issues and PRs are welcome!
We also invite you to submit links to related projects but only if:
Thanks goes to these wonderful people:
ehmicky |
thatalextaylor |
Ben Noordhuis |
Steve Lee |
Michael J. Ryan |
|---|