Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
BUILD_DIR=build
BUILD_DIR_MACOS=$(BUILD_DIR)/macos
BUILD_DIR_LINUX=$(BUILD_DIR)/linux
BUILD_DIR_WINDOWS=$(BUILD_DIR)/windows

DIST_PATH=$(BUILD_DIR)/dist
BUNDLE_PATH=$(BUILD_DIR)/bundle.js
Expand All @@ -15,11 +16,15 @@ EXE_PATH_MACOS=$(BUILD_DIR_MACOS)/$(EXE_NAME)
SERVER_PATH_LINUX=$(BUILD_DIR_LINUX)/$(SERVER_NAME)
EXE_PATH_LINUX=$(BUILD_DIR_LINUX)/$(EXE_NAME)

SERVER_PATH_WINDOWS=$(BUILD_DIR_WINDOWS)/$(SERVER_NAME).exe
EXE_PATH_WINDOWS_PS1=$(BUILD_DIR_WINDOWS)/$(EXE_NAME).ps1
EXE_PATH_WINDOWS_CMD=$(BUILD_DIR_WINDOWS)/$(EXE_NAME).cmd

VIV_VERSION ?= $(shell git describe --tags --always --dirty)

.PHONY: instruct-build
instruct-build:
@ echo 'Please run `make macos` or `make linux` to build the project'
@ echo 'Please run `make macos`, `make linux`, or `make windows` to build the project'

# ------------------------------------------------------------------------------
# MARK: platform-independent build items ---------------------------------------
Expand Down Expand Up @@ -88,6 +93,35 @@ $(EXE_PATH_LINUX): viv
mkdir -p $(BUILD_DIR_LINUX)
cp viv $(EXE_PATH_LINUX)

# ------------------------------------------------------------------------------
# MARK: windows ----------------------------------------------------------------

.PHONY: windows
windows: $(SERVER_PATH_WINDOWS) $(EXE_PATH_WINDOWS_PS1) $(EXE_PATH_WINDOWS_CMD)

$(SERVER_PATH_WINDOWS): $(BUNDLE_PATH) sea-config.json
if not exist $(BUILD_DIR_WINDOWS) mkdir $(BUILD_DIR_WINDOWS)
if exist $(SERVER_PATH_WINDOWS) del /f $(SERVER_PATH_WINDOWS)
node --experimental-sea-config sea-config.json
copy "$(shell where node)" $(SERVER_PATH_WINDOWS)
node_modules\.bin\postject $(SERVER_PATH_WINDOWS) NODE_SEA_BLOB $(BUILD_DIR)\sea-prep.blob ^
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2
@echo Windows build complete: $(SERVER_PATH_WINDOWS)

$(EXE_PATH_WINDOWS_PS1): viv.ps1
if not exist $(BUILD_DIR_WINDOWS) mkdir $(BUILD_DIR_WINDOWS)
copy viv.ps1 $(EXE_PATH_WINDOWS_PS1)

$(EXE_PATH_WINDOWS_CMD): viv.cmd
if not exist $(BUILD_DIR_WINDOWS) mkdir $(BUILD_DIR_WINDOWS)
copy viv.cmd $(EXE_PATH_WINDOWS_CMD)

# Windows build using PowerShell (alternative method)
.PHONY: windows-ps
windows-ps:
@echo Building for Windows using PowerShell...
@powershell -ExecutionPolicy Bypass -File scripts/build-windows.ps1

# ------------------------------------------------------------------------------
# MARK: configured installation ------------------------------------------------

Expand Down
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
- [produce nice looking PDFs](docs/pdfs.md) from Markdown
- automatic parsing of front matter allowing for [custom
usage](docs/front-matter.md)

- **Windows support** - works natively on Windows, macOS, and Linux

If you need any additional features, feel free to [open an
issue](https://github.com/jannis-baum/vivify/issues/new/choose) or
[contribute](docs/CONTRIBUTING.md)!
Expand Down Expand Up @@ -97,23 +98,58 @@

### Manual

#### macOS and Linux

- download & unpack the [latest
release](https://github.com/jannis-baum/vivify/releases) for your system
(macOS or Linux)
- add the two executables to your `$PATH`

#### Windows

- download & unpack the [latest
release](https://github.com/jannis-baum/vivify/releases) for Windows
- add the folder containing `vivify-server.exe` and `viv.ps1`/`viv.cmd` to your `PATH`
- use PowerShell: `viv.ps1 myfile.md` or Command Prompt: `viv myfile.md`

> [!TIP]
> For PowerShell, you can create an alias: `Set-Alias viv "C:\path\to\viv.ps1"`

### Compile yourself

#### macOS and Linux

Check failure on line 120 in README.md

View workflow job for this annotation

GitHub Actions / Lint Markdown

Multiple headings with the same content

README.md:120 MD024/no-duplicate-heading Multiple headings with the same content [Context: "#### macOS and Linux"] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md024.md

- make sure you have [`yarn`](https://yarnpkg.com), `make` and `zip` installed
- clone the repository
- run `yarn`
- run `./configure <install_dir>`
- run `make install`

#### Windows

Check failure on line 128 in README.md

View workflow job for this annotation

GitHub Actions / Lint Markdown

Multiple headings with the same content

README.md:128 MD024/no-duplicate-heading Multiple headings with the same content [Context: "#### Windows"] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md024.md

- make sure you have [Node.js](https://nodejs.org/) (v20+) and npm installed
- clone the repository
- run `npm install`
- run the build script: `powershell -ExecutionPolicy Bypass -File scripts\build-windows.ps1`
- the executables will be in `build\windows\`
- add the `build\windows\` folder to your PATH or copy the files to a location in your PATH

Check failure on line 135 in README.md

View workflow job for this annotation

GitHub Actions / Lint Markdown

Line length

README.md:135:81 MD013/line-length Line length [Expected: 80; Actual: 91] https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md

> [!TIP]
> If you are having trouble building Vivify, or you'd like more detailed build
> instructions, see our [CONTRIBUTING](docs/CONTRIBUTING.md) page

## Configuration on Windows

Vivify looks for configuration files in the following locations (in order):

1. `%APPDATA%\vivify\config.json`
2. `%USERPROFILE%\.config\vivify\config.json`
3. `%USERPROFILE%\.config\vivify.json`
4. `%USERPROFILE%\.vivify\config.json`
5. `%USERPROFILE%\.vivify.json`

See [customization](docs/customization.md) for available options.

## Get help

Is something not working or do you have any questions? [Start a
Expand Down
44 changes: 44 additions & 0 deletions build/windows/viv-debug.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@echo off
setlocal EnableDelayedExpansion

rem Debug launcher for Vivify using ts-node (runs src directly)
rem Logs WebSocket events to %TEMP%\vivify-server.log

set "INSTALL_DIR=%~dp0..\..\"
set "LOG_PATH=%TEMP%\vivify-server.log"
set "OUT_LOG=%TEMP%\vivify-server.out.log"
set "ERR_LOG=%TEMP%\vivify-server.err.log"
set "TS_NODE_BIN=%INSTALL_DIR%node_modules\ts-node\dist\bin-esm.js"

echo [%DATE% %TIME%] viv-debug starting > "%LOG_PATH%"

where node >nul 2>&1
if errorlevel 1 (
echo [%DATE% %TIME%] ERROR: node.exe not found in PATH >> "%LOG_PATH%"
exit /b 1
)

if not exist "%INSTALL_DIR%src\app.ts" (
echo [%DATE% %TIME%] ERROR: src\app.ts not found in %INSTALL_DIR% >> "%LOG_PATH%"
exit /b 1
)

if not exist "%TS_NODE_BIN%" (
echo [%DATE% %TIME%] ERROR: ts-node bin not found at %TS_NODE_BIN% >> "%LOG_PATH%"
echo [%DATE% %TIME%] Run: yarn install (or npm install) in %INSTALL_DIR% >> "%LOG_PATH%"
exit /b 1
)

set "VIV_LOG_PATH=%LOG_PATH%"
set "VIV_TIMEOUT=0"
set "NODE_ENV=development"

pushd "%INSTALL_DIR%"

rem Run server from source (ts-node CLI) and capture stdout/stderr
start /b "" node "%TS_NODE_BIN%" "%INSTALL_DIR%src\app.ts" %* > "%OUT_LOG%" 2> "%ERR_LOG%"

echo [%DATE% %TIME%] viv-debug launched ts-node server >> "%LOG_PATH%"

popd
endlocal
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"author": "Jannis Baum",
"scripts": {
"dev": "VIV_TIMEOUT=0 VIV_PORT=3000 NODE_ENV=development nodemon --ignore tests/rendering/symlinks --exec node --loader ts-node/esm src/app.ts",
"dev:win": "cross-env VIV_TIMEOUT=0 VIV_PORT=3000 NODE_ENV=development nodemon --ignore tests/rendering/symlinks --exec node --loader ts-node/esm src/app.ts",
"viv": "VIV_PORT=3000 node --loader ts-node/esm src/app.ts",
"viv:win": "cross-env VIV_PORT=3000 node --loader ts-node/esm src/app.ts",
"lint": "eslint src static",
"lint-markdown": "markdownlint-cli2 --config .github/.markdownlint-cli2.yaml",
"test": "node --loader ts-node/esm --test tests/unit/cli.ts tests/unit/alerts.ts",
Expand Down Expand Up @@ -59,6 +61,7 @@
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^8.52.0",
"@typescript-eslint/parser": "^8.52.0",
"cross-env": "^7.0.3",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
Expand Down
135 changes: 135 additions & 0 deletions scripts/build-windows.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Build Vivify for Windows.

.DESCRIPTION
This script builds the vivify-server executable for Windows using Node.js
Single Executable Applications (SEA) feature.

.PARAMETER Clean
Clean the build directory before building.

.EXAMPLE
.\scripts\build-windows.ps1
Builds Vivify for Windows.

.EXAMPLE
.\scripts\build-windows.ps1 -Clean
Cleans and rebuilds Vivify for Windows.
#>

[CmdletBinding()]
param(
[switch]$Clean
)

$ErrorActionPreference = "Stop"

# Configuration
$BuildDir = "build"
$WindowsBuildDir = "$BuildDir\windows"
$ServerName = "vivify-server.exe"
$BundlePath = "$BuildDir\bundle.js"
$StaticPath = "$BuildDir\static.zip"
$SeaConfigPath = "sea-config.json"
$SeaBlobPath = "$BuildDir\sea-prep.blob"

function Write-Step {
param([string]$Message)
Write-Host "`n[$([DateTime]::Now.ToString('HH:mm:ss'))] $Message" -ForegroundColor Cyan
}

function Test-Command {
param([string]$Command)
$null = Get-Command $Command -ErrorAction SilentlyContinue
return $?
}

# Check prerequisites
Write-Step "Checking prerequisites..."

if (-not (Test-Command "node")) {
Write-Error "Node.js is not installed or not in PATH"
exit 1
}

if (-not (Test-Command "npm")) {
Write-Error "npm is not installed or not in PATH"
exit 1
}

$nodeVersion = node --version
Write-Host " Node.js version: $nodeVersion"

# Check if Node.js version supports SEA (>= 20.0.0)
$versionMatch = $nodeVersion -match 'v(\d+)'
if ($matches[1] -lt 20) {
Write-Warning "Node.js 20+ is recommended for SEA support. Current: $nodeVersion"
}

# Clean if requested
if ($Clean) {
Write-Step "Cleaning build directory..."
if (Test-Path $BuildDir) {
Remove-Item -Recurse -Force $BuildDir
}
}

# Create build directories
Write-Step "Creating build directories..."
New-Item -ItemType Directory -Force -Path $WindowsBuildDir | Out-Null

# Install dependencies if needed
if (-not (Test-Path "node_modules")) {
Write-Step "Installing dependencies..."
npm install
}

# Build TypeScript
Write-Step "Compiling TypeScript..."
npx tsc

# Create static.zip
Write-Step "Creating static.zip..."
if (Test-Path $StaticPath) {
Remove-Item $StaticPath
}
Compress-Archive -Path "static\*" -DestinationPath $StaticPath

# Build with webpack
Write-Step "Building with webpack..."
$env:VIV_VERSION = git describe --tags --always --dirty 2>$null
if (-not $env:VIV_VERSION) {
$env:VIV_VERSION = "dev"
}
npx webpack

# Generate SEA blob
Write-Step "Generating SEA blob..."
node --experimental-sea-config $SeaConfigPath

# Copy node.exe
Write-Step "Creating executable..."
$nodeExe = (Get-Command node).Source
$serverPath = "$WindowsBuildDir\$ServerName"
Copy-Item $nodeExe $serverPath -Force

# Inject SEA blob
Write-Step "Injecting SEA blob..."
npx postject $serverPath NODE_SEA_BLOB $SeaBlobPath `
--sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2

# Copy scripts
Write-Step "Copying launcher scripts..."
Copy-Item "viv.ps1" "$WindowsBuildDir\viv.ps1" -Force
Copy-Item "viv.cmd" "$WindowsBuildDir\viv.cmd" -Force

Write-Host "`n" -NoNewline
Write-Host "Build complete!" -ForegroundColor Green
Write-Host "Output directory: $WindowsBuildDir"
Write-Host "Files:"
Get-ChildItem $WindowsBuildDir | ForEach-Object {
$size = if ($_.Length -gt 1MB) { "{0:N2} MB" -f ($_.Length / 1MB) } else { "{0:N2} KB" -f ($_.Length / 1KB) }
Write-Host " $($_.Name) ($size)"
}
21 changes: 20 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ import { globSync } from 'glob';
import { homedir } from 'os';
import path from 'path';

// Platform detection
const isWindows = process.platform === 'win32';

// Check if a path is absolute on any platform
const isAbsolutePath = (p: string): boolean => {
if (isWindows) {
// Windows: C:\ or C:/ or \\ (UNC)
return /^[a-zA-Z]:[\\/]/.test(p) || p.startsWith('\\\\');
}
return p.startsWith('/');
};

// NOTE: this type does not directly correspond to the config file: see
// defaultConfig, envConfigs and configFileBlocked
type Config = {
Expand Down Expand Up @@ -47,13 +59,19 @@ const envConfigs: [string, keyof Config][] = [
// configs that can't be set through the config file
const configFileBlocked: (keyof Config)[] = ['port'];

// Build config paths - on Windows use AppData as well
const configPaths = [
path.join(homedir(), '.config', 'vivify', 'config.json'),
path.join(homedir(), '.config', 'vivify.json'),
path.join(homedir(), '.vivify', 'config.json'),
path.join(homedir(), '.vivify.json'),
];

// On Windows, also check AppData/Roaming
if (isWindows && process.env.APPDATA) {
configPaths.unshift(path.join(process.env.APPDATA, 'vivify', 'config.json'));
}

// read contents of file at paths or files at paths
const getFileContents = (
paths: string[] | string | undefined,
Expand All @@ -66,7 +84,8 @@ const getFileContents = (
if (resolved[0] === '~') {
resolved = path.join(homedir(), p.slice(1));
}
if (resolved[0] !== '/' && baseDir !== undefined) {
// Check for relative paths - use isAbsolutePath for cross-platform support
if (!isAbsolutePath(resolved) && baseDir !== undefined) {
resolved = path.join(baseDir, resolved);
}
return globSync(resolved)
Expand Down
Loading
Loading