1
mirror of https://github.com/jakejarvis/hugo-extended.git synced 2026-06-12 08:45:27 -04:00

feat: allow environment variable overrides of default behaviors (#182)

This commit is contained in:
2026-01-08 21:23:53 -05:00
committed by GitHub
parent d14ea4dbd9
commit b409823e55
15 changed files with 1288 additions and 188 deletions
+8 -6
View File
@@ -16,11 +16,13 @@ jobs:
name: Bump to latest Hugo version
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: actions/setup-node@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
@@ -64,7 +66,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
LATEST="${{ steps.hugo.outputs.version }}"
BRANCH_NAME="hugo-v${LATEST}"
BRANCH_NAME="autobump-hugo-v${LATEST}"
# Check if a PR for this version already exists
EXISTING_PR=$(gh pr list --head "$BRANCH_NAME" --json number --jq '.[0].number // empty')
@@ -87,7 +89,7 @@ jobs:
if: steps.check.outputs.needs_update == 'true' && steps.pr_check.outputs.pr_exists == 'false'
run: |
LATEST="${{ steps.hugo.outputs.version }}"
BRANCH_NAME="hugo-v${LATEST}"
BRANCH_NAME="autobump-hugo-v${LATEST}"
# Create new branch
git checkout -b "$BRANCH_NAME"
@@ -103,7 +105,7 @@ jobs:
if: steps.check.outputs.needs_update == 'true' && steps.pr_check.outputs.pr_exists == 'false'
run: |
LATEST="${{ steps.hugo.outputs.version }}"
BRANCH_NAME="hugo-v${LATEST}"
BRANCH_NAME="autobump-hugo-v${LATEST}"
git commit -m "chore: bump to Hugo v${LATEST}"
git push origin "$BRANCH_NAME"
@@ -114,7 +116,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
LATEST="${{ steps.hugo.outputs.version }}"
BRANCH_NAME="hugo-v${LATEST}"
BRANCH_NAME="autobump-hugo-v${LATEST}"
gh pr create \
--title "chore: bump to Hugo v${LATEST}" \
+16 -2
View File
@@ -14,20 +14,34 @@ jobs:
name: Publish to NPM
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
registry-url: "https://registry.npmjs.org"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: npm install -g npm@latest && npm ci
- name: Generate types
run: npm run generate-types
- name: Lint
run: npm run lint
- name: Type check
run: npm run typecheck
- name: Build
run: npm run build
- name: Publish
run: npm publish
+78 -16
View File
@@ -19,21 +19,33 @@ jobs:
matrix:
node: ["20", "22", "24"]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: "npm"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: npm ci
- name: Generate types
run: npm run generate-types
- name: Lint
run: npm run lint
- name: Type check
run: npm run typecheck
- name: Build
run: npm run build
- name: Run unit tests
run: npm run test:unit
@@ -48,17 +60,27 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
node: ["20", "22", "24"]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: "npm"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: npm ci
- name: Generate types
run: npm run generate-types
- name: Build
run: npm run build
- name: Run integration tests
run: npm run test:integration
@@ -72,17 +94,27 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "npm"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: npm ci
- name: Generate types
run: npm run generate-types
- name: Build
run: npm run build
- name: Run E2E tests
run: npm run test:e2e
@@ -96,12 +128,18 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "npm"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: npm ci
@@ -187,12 +225,18 @@ jobs:
needs: unit
runs-on: macos-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "npm"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: npm ci
@@ -236,12 +280,18 @@ jobs:
needs: unit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "npm"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: npm ci
@@ -293,12 +343,18 @@ jobs:
needs: unit
runs-on: windows-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "npm"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: npm ci
@@ -341,12 +397,18 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- name: Checkout
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "npm"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: npm ci
+33 -1
View File
@@ -26,9 +26,15 @@ Notes for LLM coding agents working on `hugo-extended`.
- **Binary installation**: `src/lib/install.ts`
- Downloads Hugo release assets and verifies SHA-256 checksums.
- **macOS**: uses `sudo installer -pkg ... -target /` (and then symlinks `bin/hugo` -> `/usr/local/bin/hugo`).
- **macOS v0.153.0+**: uses `sudo installer -pkg ... -target /` (and then symlinks `bin/hugo` -> `/usr/local/bin/hugo`).
- **macOS pre-v0.153.0**: extracts `.tar.gz` archive into `bin/` (no sudo required).
- **non-macOS**: extracts archive into `bin/` and `chmod +x`.
- **Environment variables**: `src/lib/env.ts`
- Centralized handling of all `HUGO_*` environment variables.
- Exports `getEnvConfig()` for reading parsed config, `logger` for quiet-aware logging.
- Exports `ENV_VAR_DOCS` for programmatic access to variable metadata.
- **Postinstall**: `postinstall.js`
- For published packages (where `dist/` exists), runs the compiled installer.
- For repo/dev/CI (where `dist/` may not exist), exits successfully and skips installation.
@@ -110,3 +116,29 @@ npm run test:coverage # coverage via v8
- If you touch exports in `src/hugo.ts`:
- Remember: consumers rely on the **default export being callable** (binary path) and having builder methods attached.
- If you touch environment variables (`src/lib/env.ts`):
- All env vars are defined in `ENV_VARS` with name, aliases, parse function, and description.
- Boolean env vars accept: `1`, `true`, `yes`, `on` (case-insensitive).
- Use `getEnvConfig()` to read config; use `logger.info/warn/error` for quiet-aware output.
- `postinstall.js` has its own minimal env parsing (can't import TypeScript modules).
## Environment variables reference
| Variable | Type | Description |
|----------|------|-------------|
| `HUGO_OVERRIDE_VERSION` | string | Install a different Hugo version (ignores package.json) |
| `HUGO_NO_EXTENDED` | boolean | Force vanilla Hugo instead of Extended |
| `HUGO_SKIP_DOWNLOAD` | boolean | Skip postinstall binary download |
| `HUGO_BIN_PATH` | string | Use a pre-existing Hugo binary |
| `HUGO_MIRROR_BASE_URL` | string | Custom download mirror URL |
| `HUGO_SKIP_CHECKSUM` | boolean | Skip SHA-256 verification |
| `HUGO_QUIET` | boolean | Suppress installation output |
Some variables have aliases (e.g., `HUGO_FORCE_STANDARD``HUGO_NO_EXTENDED`, `HUGO_SILENT``HUGO_QUIET`). Check `ENV_VARS` in `src/lib/env.ts` for the full list.
### Version-dependent behavior
- **macOS v0.153.0+**: Hugo ships as `.pkg` installer, requires `sudo`.
- **macOS pre-v0.153.0**: Hugo ships as `.tar.gz`, extracted to `bin/` directly.
- The `usesMacOSPkg(version)` and `compareVersions(a, b)` utilities in `src/lib/utils.ts` handle this.
+30
View File
@@ -212,6 +212,36 @@ Hugo Extended is automatically used on supported platforms:
| Windows | ARM64 | ❌ (vanilla Hugo) |
| FreeBSD | x64 | ❌ (vanilla Hugo) |
## Environment Variables
Customize installation and runtime behavior with these environment variables:
| Variable | Description |
|----------|-------------|
| `HUGO_OVERRIDE_VERSION` | Install a specific Hugo version instead of the package version. Example: `HUGO_OVERRIDE_VERSION=0.139.0 npm install` |
| `HUGO_NO_EXTENDED` | Force vanilla Hugo instead of Extended edition. Example: `HUGO_NO_EXTENDED=1 npm install` |
| `HUGO_SKIP_DOWNLOAD` | Skip the postinstall binary download entirely. Useful for CI caching or Docker layer optimization. |
| `HUGO_BIN_PATH` | Use a pre-existing Hugo binary instead of the bundled one. Example: `HUGO_BIN_PATH=/usr/local/bin/hugo` |
| `HUGO_MIRROR_BASE_URL` | Download from a custom mirror instead of GitHub releases. Example: `HUGO_MIRROR_BASE_URL=https://mirror.example.com/hugo` |
| `HUGO_SKIP_CHECKSUM` | Skip SHA-256 checksum verification. **Use with caution.** |
| `HUGO_QUIET` | Suppress installation progress output. |
### Examples
```sh
# Install a specific older version
HUGO_OVERRIDE_VERSION=0.139.0 npm install hugo-extended
# Skip download for CI caching (when binary is already cached)
HUGO_SKIP_DOWNLOAD=1 npm ci
# Use smaller vanilla Hugo (no SCSS support)
HUGO_NO_EXTENDED=1 npm install hugo-extended
# Use a corporate mirror
HUGO_MIRROR_BASE_URL=https://internal.example.com/hugo npm install hugo-extended
```
## Troubleshooting
### Hugo binary not found
+83 -104
View File
@@ -24,8 +24,7 @@
"@types/tar": "^6.1.13",
"@vitest/coverage-v8": "^4.0.16",
"tinyexec": "^1.0.2",
"tsdown": "^0.19.0-beta.3",
"tsx": "^4.21.0",
"tsdown": "^0.19.0-beta.5",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
},
@@ -827,9 +826,9 @@
}
},
"node_modules/@oxc-project/types": {
"version": "0.106.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.106.0.tgz",
"integrity": "sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg==",
"version": "0.107.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.107.0.tgz",
"integrity": "sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -850,9 +849,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.58.tgz",
"integrity": "sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-beta.59.tgz",
"integrity": "sha512-6yLLgyswYwiCfls9+hoNFY9F8TQdwo15hpXDHzlAR0X/GojeKF+AuNcXjYNbOJ4zjl/5D6lliE8CbpB5t1OWIQ==",
"cpu": [
"arm64"
],
@@ -867,9 +866,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.58.tgz",
"integrity": "sha512-wFxUymI/5R8bH8qZFYDfAxAN9CyISEIYke+95oZPiv6EWo88aa5rskjVcCpKA532R+klFmdqjbbaD56GNmTF4Q==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-beta.59.tgz",
"integrity": "sha512-hqGXRc162qCCIOAcHN2Cw4eXiVTwYsMFLOhAy1IG2CxY+dwc/l4Ga+dLPkLor3Ikqy5WDn+7kxHbbh6EmshEpQ==",
"cpu": [
"arm64"
],
@@ -884,9 +883,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.58.tgz",
"integrity": "sha512-ybp3MkPj23VDV9PhtRwdU5qrGhlViWRV5BjKwO6epaSlUD5lW0WyY+roN3ZAzbma/9RrMTgZ/a/gtQq8YXOcqw==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-beta.59.tgz",
"integrity": "sha512-ezvvGuhteE15JmMhJW0wS7BaXmhwLy1YHeEwievYaPC1PgGD86wgBKfOpHr9tSKllAXbCe0BeeMvasscWLhKdA==",
"cpu": [
"x64"
],
@@ -901,9 +900,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.58.tgz",
"integrity": "sha512-Evxj3yh7FWvyklUYZa0qTVT9N2zX9TPDqGF056hl8hlCZ9/ndQ2xMv6uw9PD1VlLpukbsqL+/C6M0qwipL0QMg==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-beta.59.tgz",
"integrity": "sha512-4fhKVJiEYVd5n6no/mrL3LZ9kByfCGwmONOrdtvx8DJGDQhehH/q3RfhG3V/4jGKhpXgbDjpIjkkFdybCTcgew==",
"cpu": [
"x64"
],
@@ -918,9 +917,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.58.tgz",
"integrity": "sha512-tYeXprDOrEgVHUbPXH6MPso4cM/c6RTkmJNICMQlYdki4hGMh92aj3yU6CKs+4X5gfG0yj5kVUw/L4M685SYag==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-beta.59.tgz",
"integrity": "sha512-T3Y52sW6JAhvIqArBw+wtjNU1Ieaz4g0NBxyjSJoW971nZJBZygNlSYx78G4cwkCmo1dYTciTPDOnQygLV23pA==",
"cpu": [
"arm"
],
@@ -935,9 +934,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.58.tgz",
"integrity": "sha512-N78vmZzP6zG967Ohr+MasCjmKtis0geZ1SOVmxrA0/bklTQSzH5kHEjW5Qn+i1taFno6GEre1E40v0wuWsNOQw==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-beta.59.tgz",
"integrity": "sha512-NIW40jQDSQap2KDdmm9z3B/4OzWJ6trf8dwx3FD74kcQb3v34ThsBFTtzE5KjDuxnxgUlV+DkAu+XgSMKrgufw==",
"cpu": [
"arm64"
],
@@ -952,9 +951,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.58.tgz",
"integrity": "sha512-l+p4QVtG72C7wI2SIkNQw/KQtSjuYwS3rV6AKcWrRBF62ClsFUcif5vLaZIEbPrCXu5OFRXigXFJnxYsVVZqdQ==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-beta.59.tgz",
"integrity": "sha512-CCKEk+H+8c0WGe/8n1E20n85Tq4Pv+HNAbjP1KfUXW+01aCWSMjU56ChNrM2tvHnXicfm7QRNoZyfY8cWh7jLQ==",
"cpu": [
"arm64"
],
@@ -969,9 +968,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.58.tgz",
"integrity": "sha512-urzJX0HrXxIh0FfxwWRjfPCMeInU9qsImLQxHBgLp5ivji1EEUnOfux8KxPPnRQthJyneBrN2LeqUix9DYrNaQ==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-beta.59.tgz",
"integrity": "sha512-VlfwJ/HCskPmQi8R0JuAFndySKVFX7yPhE658o27cjSDWWbXVtGkSbwaxstii7Q+3Rz87ZXN+HLnb1kd4R9Img==",
"cpu": [
"x64"
],
@@ -986,9 +985,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.58.tgz",
"integrity": "sha512-7ijfVK3GISnXIwq/1FZo+KyAUJjL3kWPJ7rViAL6MWeEBhEgRzJ0yEd9I8N9aut8Y8ab+EKFJyRNMWZuUBwQ0A==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-beta.59.tgz",
"integrity": "sha512-kuO92hTRyGy0Ts3Nsqll0rfO8eFsEJe9dGQGktkQnZ2hrJrDVN0y419dMgKy/gB2S2o7F2dpWhpfQOBehZPwVA==",
"cpu": [
"x64"
],
@@ -1003,9 +1002,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.58.tgz",
"integrity": "sha512-/m7sKZCS+cUULbzyJTIlv8JbjNohxbpAOA6cM+lgWgqVzPee3U6jpwydrib328JFN/gF9A99IZEnuGYqEDJdww==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-beta.59.tgz",
"integrity": "sha512-PXAebvNL4sYfCqi8LdY4qyFRacrRoiPZLo3NoUmiTxm7MPtYYR8CNtBGNokqDmMuZIQIecRaD/jbmFAIDz7DxQ==",
"cpu": [
"arm64"
],
@@ -1020,9 +1019,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.58.tgz",
"integrity": "sha512-6SZk7zMgv+y3wFFQ9qE5P9NnRHcRsptL1ypmudD26PDY+PvFCvfHRkJNfclWnvacVGxjowr7JOL3a9fd1wWhUw==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-beta.59.tgz",
"integrity": "sha512-yJoklQg7XIZq8nAg0bbkEXcDK6sfpjxQGxpg2Nd6ERNtvg+eOaEBRgPww0BVTrYFQzje1pB5qPwC2VnJHT3koQ==",
"cpu": [
"wasm32"
],
@@ -1037,9 +1036,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.58.tgz",
"integrity": "sha512-sFqfYPnBZ6xBhMkadB7UD0yjEDRvs7ipR3nCggblN+N4ODCXY6qhg/bKL39+W+dgQybL7ErD4EGERVbW9DAWvg==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-beta.59.tgz",
"integrity": "sha512-ljZ4+McmCbIuZwEBaoGtiG8Rq2nJjaXEnLEIx+usWetXn1ECjXY0LAhkELxOV6ytv4ensEmoJJ8nXg47hRMjlw==",
"cpu": [
"arm64"
],
@@ -1054,9 +1053,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.58.tgz",
"integrity": "sha512-AnFWJdAqB8+IDPcGrATYs67Kik/6tnndNJV2jGRmwlbeNiQQ8GhRJU8ETRlINfII0pqi9k4WWLnb00p1QCxw/Q==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-beta.59.tgz",
"integrity": "sha512-bMY4tTIwbdZljW+xe/ln1hvs0SRitahQSXfWtvgAtIzgSX9Ar7KqJzU7lRm33YTRFIHLULRi53yNjw9nJGd6uQ==",
"cpu": [
"x64"
],
@@ -1071,9 +1070,9 @@
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.58.tgz",
"integrity": "sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.59.tgz",
"integrity": "sha512-aoh6LAJRyhtazs98ydgpNOYstxUlsOV1KJXcpf/0c0vFcUA8uyd/hwKRhqE/AAPNqAho9RliGsvitCoOzREoVA==",
"dev": true,
"license": "MIT"
},
@@ -1702,13 +1701,6 @@
"js-tokens": "^9.0.1"
}
},
"node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/birpc": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz",
@@ -2010,6 +2002,13 @@
"node": ">=8"
}
},
"node_modules/js-tokens": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
"integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
"dev": true,
"license": "MIT"
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
@@ -2213,14 +2212,14 @@
}
},
"node_modules/rolldown": {
"version": "1.0.0-beta.58",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.58.tgz",
"integrity": "sha512-v1FCjMZCan7f+xGAHBi+mqiE4MlH7I+SXEHSQSJoMOGNNB2UYtvMiejsq9YuUOiZjNeUeV/a21nSFbrUR+4ZCQ==",
"version": "1.0.0-beta.59",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-beta.59.tgz",
"integrity": "sha512-Slm000Gd8/AO9z4Kxl4r8mp/iakrbAuJ1L+7ddpkNxgQ+Vf37WPvY63l3oeyZcfuPD1DRrUYBsRPIXSOhvOsmw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.106.0",
"@rolldown/pluginutils": "1.0.0-beta.58"
"@oxc-project/types": "=0.107.0",
"@rolldown/pluginutils": "1.0.0-beta.59"
},
"bin": {
"rolldown": "bin/cli.mjs"
@@ -2229,19 +2228,19 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.0-beta.58",
"@rolldown/binding-darwin-arm64": "1.0.0-beta.58",
"@rolldown/binding-darwin-x64": "1.0.0-beta.58",
"@rolldown/binding-freebsd-x64": "1.0.0-beta.58",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.58",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.58",
"@rolldown/binding-linux-arm64-musl": "1.0.0-beta.58",
"@rolldown/binding-linux-x64-gnu": "1.0.0-beta.58",
"@rolldown/binding-linux-x64-musl": "1.0.0-beta.58",
"@rolldown/binding-openharmony-arm64": "1.0.0-beta.58",
"@rolldown/binding-wasm32-wasi": "1.0.0-beta.58",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.58",
"@rolldown/binding-win32-x64-msvc": "1.0.0-beta.58"
"@rolldown/binding-android-arm64": "1.0.0-beta.59",
"@rolldown/binding-darwin-arm64": "1.0.0-beta.59",
"@rolldown/binding-darwin-x64": "1.0.0-beta.59",
"@rolldown/binding-freebsd-x64": "1.0.0-beta.59",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.59",
"@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.59",
"@rolldown/binding-linux-arm64-musl": "1.0.0-beta.59",
"@rolldown/binding-linux-x64-gnu": "1.0.0-beta.59",
"@rolldown/binding-linux-x64-musl": "1.0.0-beta.59",
"@rolldown/binding-openharmony-arm64": "1.0.0-beta.59",
"@rolldown/binding-wasm32-wasi": "1.0.0-beta.59",
"@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.59",
"@rolldown/binding-win32-x64-msvc": "1.0.0-beta.59"
}
},
"node_modules/rolldown-plugin-dts": {
@@ -2470,9 +2469,9 @@
}
},
"node_modules/tsdown": {
"version": "0.19.0-beta.3",
"resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.19.0-beta.3.tgz",
"integrity": "sha512-Ud75SBmTap0kDf9hs31yBBlU0iAV17gtZgTJlW6nG/e4J6wXPXwQtUXt/Fck4XSmHXXgSuYRwGrjF6AxTLwk+Q==",
"version": "0.19.0-beta.5",
"resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.19.0-beta.5.tgz",
"integrity": "sha512-2/Mn1jqkJS65tD1oXqld7cShhx/Gg8etv4Oy1eEm0Cl+hEbYmXVQtsV9OL75X4ObbYu42U0vO7g6KyuAaEwklg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2484,14 +2483,14 @@
"import-without-cache": "^0.2.5",
"obug": "^2.1.1",
"picomatch": "^4.0.3",
"rolldown": "1.0.0-beta.58",
"rolldown": "1.0.0-beta.59",
"rolldown-plugin-dts": "^0.20.0",
"semver": "^7.7.3",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
"tree-kill": "^1.2.2",
"unconfig-core": "^7.4.2",
"unrun": "^0.2.22"
"unrun": "^0.2.23"
},
"bin": {
"tsdown": "dist/run.mjs"
@@ -2539,26 +2538,6 @@
"license": "0BSD",
"optional": true
},
"node_modules/tsx": {
"version": "4.21.0",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.27.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
"tsx": "dist/cli.mjs"
},
"engines": {
"node": ">=18.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -2595,13 +2574,13 @@
"license": "MIT"
},
"node_modules/unrun": {
"version": "0.2.22",
"resolved": "https://registry.npmjs.org/unrun/-/unrun-0.2.22.tgz",
"integrity": "sha512-vlQce4gTLNyCZxGylEQXGG+fSrrEFWiM/L8aghtp+t6j8xXh+lmsBtQJknG7ZSvv7P+/MRgbQtHWHBWk981uTg==",
"version": "0.2.24",
"resolved": "https://registry.npmjs.org/unrun/-/unrun-0.2.24.tgz",
"integrity": "sha512-xa4/O5q2jmI6EqxweJ+sOy5cyORZWcsgmi8pmABVSUyg24Fh44qJrneUHavZEMsbJbghHYWKSraFy5hDCb/m4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"rolldown": "1.0.0-beta.58"
"rolldown": "1.0.0-beta.59"
},
"bin": {
"unrun": "dist/cli.mjs"
@@ -2622,9 +2601,9 @@
}
},
"node_modules/vite": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"dependencies": {
+2 -3
View File
@@ -44,7 +44,7 @@
"scripts": {
"build": "tsdown",
"dev": "tsdown --watch",
"generate-types": "tsx scripts/generate-types.ts",
"generate-types": "bun scripts/generate-types.ts",
"lint": "biome check",
"lint:fix": "biome check --write",
"typecheck": "tsc --noEmit",
@@ -68,8 +68,7 @@
"@types/tar": "^6.1.13",
"@vitest/coverage-v8": "^4.0.16",
"tinyexec": "^1.0.2",
"tsdown": "^0.19.0-beta.3",
"tsx": "^4.21.0",
"tsdown": "^0.19.0-beta.5",
"typescript": "^5.9.3",
"vitest": "^4.0.16"
},
+20
View File
@@ -11,12 +11,32 @@ import { fileURLToPath } from "node:url";
*
* During development/CI (before build), the dist folder won't exist and this script will exit gracefully.
* For published packages, the dist folder is included and installation will proceed.
*
* Environment variables:
* - HUGO_SKIP_DOWNLOAD: Skip installation entirely (useful for CI caching, Docker layers)
*/
const __dirname = dirname(fileURLToPath(import.meta.url));
const installPath = join(__dirname, "dist", "lib", "install.mjs");
/**
* Check if an environment variable is set to a truthy value.
* @param {string} name - Environment variable name
* @returns {boolean}
*/
function isEnvTruthy(name) {
const value = process.env[name];
if (!value) return false;
return ["1", "true", "yes", "on"].includes(value.toLowerCase().trim());
}
async function run() {
// Skip installation if HUGO_SKIP_DOWNLOAD is set
if (isEnvTruthy("HUGO_SKIP_DOWNLOAD")) {
console.log("Skipping Hugo installation (HUGO_SKIP_DOWNLOAD is set)");
process.exit(0);
}
// Skip installation if dist folder doesn't exist (development/CI environment)
if (!existsSync(installPath)) {
console.log(
+1
View File
@@ -1,6 +1,7 @@
import fs from "node:fs/promises";
import path from "node:path";
import { x } from "tinyexec";
import hugo from "../src/hugo";
const OUT_DIR = "src/generated";
+19 -3
View File
@@ -1,8 +1,9 @@
import { spawn } from "node:child_process";
import type { HugoCommand, HugoOptionsFor } from "./generated/types";
import { buildArgs } from "./lib/args";
import { getEnvConfig } from "./lib/env";
import install from "./lib/install";
import { doesBinExist, getBinPath } from "./lib/utils";
import { doesBinExist, getBinPath, logger } from "./lib/utils";
/**
* Gets the path to the Hugo binary, automatically installing it if it's missing.
@@ -14,8 +15,11 @@ import { doesBinExist, getBinPath } from "./lib/utils";
* This handles the case where Hugo may mysteriously disappear (see issue #81),
* ensuring the binary is always available when this function is called.
*
* Environment variables that affect behavior:
* - HUGO_BIN_PATH: Use a custom binary path (skips auto-install if missing)
*
* @returns A promise that resolves with the absolute path to the Hugo binary
* @throws {Error} If installation fails or the platform is unsupported
* @throws {Error} If installation fails, the platform is unsupported, or custom binary is missing
*
* @example
* ```typescript
@@ -26,15 +30,24 @@ import { doesBinExist, getBinPath } from "./lib/utils";
* ```
*/
export const getHugoBinary = async (): Promise<string> => {
const envConfig = getEnvConfig();
const bin = getBinPath();
// If using a custom binary path, don't try to auto-install
if (envConfig.binPath) {
if (!doesBinExist(bin)) {
throw new Error(`Custom Hugo binary not found at HUGO_BIN_PATH: ${bin}`);
}
return bin;
}
// A fix for fleeting ENOENT errors, where Hugo seems to disappear. For now,
// just reinstall Hugo when it's missing and then continue normally like
// nothing happened.
// See: https://github.com/jakejarvis/hugo-extended/issues/81
if (!doesBinExist(bin)) {
// Hugo isn't there for some reason. Try re-installing.
console.info("⚠️ Hugo is missing, reinstalling now...");
logger.info("⚠️ Hugo is missing, reinstalling now...");
await install();
}
@@ -361,3 +374,6 @@ export default Object.assign(hugoCompat, hugo);
// Re-export types for convenience
export type { HugoCommand, HugoOptionsFor } from "./generated/types";
// Re-export env utilities for advanced usage
export { ENV_VAR_DOCS, getEnvConfig, type HugoEnvConfig } from "./lib/env";
+278
View File
@@ -0,0 +1,278 @@
/**
* Centralized environment variable handling for hugo-extended.
*
* All environment variables are prefixed with `HUGO_` and provide ways to
* customize the installation and runtime behavior of the package.
*
* @module
*/
/**
* Environment variable configuration schema.
* Each entry defines a variable's name, aliases, and parsing behavior.
*/
interface EnvVarConfig<T> {
/** Primary environment variable name */
name: string;
/** Alternative names that also work (for convenience) */
aliases?: string[];
/** Description for documentation */
description: string;
/** Parse the raw string value into the desired type */
parse: (value: string | undefined) => T;
/** Default value when not set */
defaultValue: T;
}
/**
* Parses a boolean environment variable.
* Truthy values: "1", "true", "yes", "on" (case-insensitive)
* Falsy values: "0", "false", "no", "off", undefined, empty string
*/
function parseBoolean(value: string | undefined): boolean {
if (!value) return false;
const normalized = value.toLowerCase().trim();
return ["1", "true", "yes", "on"].includes(normalized);
}
/**
* Parses a string environment variable (returns undefined if empty).
*/
function parseString(value: string | undefined): string | undefined {
if (!value || value.trim() === "") return undefined;
return value.trim();
}
/**
* Parses a version string, stripping any leading "v" prefix.
*/
function parseVersion(value: string | undefined): string | undefined {
const str = parseString(value);
if (!str) return undefined;
return str.startsWith("v") ? str.slice(1) : str;
}
/**
* Gets the first defined value from a list of environment variable names.
*/
function getFirstDefined(names: string[]): string | undefined {
for (const name of names) {
const value = process.env[name];
if (value !== undefined && value !== "") {
return value;
}
}
return undefined;
}
/**
* All supported environment variable configurations.
*/
const ENV_VARS = {
/**
* Override the Hugo version to install.
* When set, ignores the version from package.json.
*
* Intentionally not aliased to HUGO_VERSION to avoid confusion and
* conflicts with Netlify, etc.
*
* @example HUGO_OVERRIDE_VERSION=0.139.0 npm install hugo-extended
*/
overrideVersion: {
name: "HUGO_OVERRIDE_VERSION",
aliases: [],
description: "Override the Hugo version to install",
parse: parseVersion,
defaultValue: undefined,
} satisfies EnvVarConfig<string | undefined>,
/**
* Force installation of vanilla Hugo instead of Extended.
* Useful when SCSS/SASS features aren't needed or to reduce binary size.
*
* @example HUGO_NO_EXTENDED=1 npm install hugo-extended
*/
forceStandard: {
name: "HUGO_NO_EXTENDED",
aliases: ["HUGO_FORCE_STANDARD"],
description: "Force vanilla Hugo instead of Extended edition",
parse: parseBoolean,
defaultValue: false,
} satisfies EnvVarConfig<boolean>,
/**
* Skip the postinstall Hugo binary download entirely.
* Useful for CI caching, Docker layer optimization, or when Hugo is
* already installed system-wide.
*
* @example HUGO_SKIP_DOWNLOAD=1 npm ci
*/
skipInstall: {
name: "HUGO_SKIP_DOWNLOAD",
aliases: [],
description: "Skip the postinstall binary download",
parse: parseBoolean,
defaultValue: false,
} satisfies EnvVarConfig<boolean>,
/**
* Use a pre-existing Hugo binary instead of the bundled one.
* When set, the package will use this path for all Hugo operations.
*
* @example HUGO_BIN_PATH=/usr/local/bin/hugo npm start
*/
binPath: {
name: "HUGO_BIN_PATH",
aliases: [],
description: "Path to a pre-existing Hugo binary",
parse: parseString,
defaultValue: undefined,
} satisfies EnvVarConfig<string | undefined>,
/**
* Override the base URL for downloading Hugo releases.
* Useful for air-gapped environments, corporate mirrors, or faster
* regional mirrors.
*
* The URL should be the base path where release files are hosted.
* The version and filename will be appended automatically.
*
* @example HUGO_MIRROR_BASE_URL=https://mirror.example.com/hugo npm install
*/
downloadBaseUrl: {
name: "HUGO_MIRROR_BASE_URL",
aliases: [],
description: "Custom base URL for Hugo release downloads",
parse: parseString,
defaultValue: undefined,
} satisfies EnvVarConfig<string | undefined>,
/**
* Skip SHA-256 checksum verification of downloaded files.
* Use with caution - only recommended for trusted mirrors or development.
*
* @example HUGO_SKIP_CHECKSUM=1 npm install hugo-extended
*/
skipChecksum: {
name: "HUGO_SKIP_CHECKSUM",
aliases: ["HUGO_SKIP_VERIFY"],
description: "Skip SHA-256 checksum verification",
parse: parseBoolean,
defaultValue: false,
} satisfies EnvVarConfig<boolean>,
/**
* Suppress installation progress output.
* Useful for cleaner CI logs or scripted automation.
*
* @example HUGO_QUIET=1 npm install hugo-extended
*/
quiet: {
name: "HUGO_QUIET",
aliases: ["HUGO_SILENT"],
description: "Suppress installation progress output",
parse: parseBoolean,
defaultValue: false,
} satisfies EnvVarConfig<boolean>,
} as const;
/**
* Typed environment configuration object.
* Provides a clean API for accessing all Hugo environment variables.
*/
export interface HugoEnvConfig {
/** Override the Hugo version to install (ignores package.json) */
overrideVersion: string | undefined;
/** Force vanilla Hugo instead of Extended edition */
forceStandard: boolean;
/** Skip the postinstall binary download */
skipInstall: boolean;
/** Path to a pre-existing Hugo binary */
binPath: string | undefined;
/** Custom base URL for Hugo release downloads */
downloadBaseUrl: string | undefined;
/** Skip SHA-256 checksum verification */
skipChecksum: boolean;
/** Suppress installation progress output */
quiet: boolean;
}
/**
* Reads and parses all Hugo environment variables.
*
* This function reads from `process.env` each time it's called,
* so it will pick up any runtime changes to environment variables.
*
* @returns Parsed environment configuration
*
* @example
* ```typescript
* import { getEnvConfig } from './lib/env';
*
* const config = getEnvConfig();
* if (config.skipInstall) {
* console.log('Skipping installation');
* }
* ```
*/
export function getEnvConfig(): HugoEnvConfig {
return {
overrideVersion: ENV_VARS.overrideVersion.parse(
getFirstDefined([
ENV_VARS.overrideVersion.name,
...(ENV_VARS.overrideVersion.aliases ?? []),
]),
),
forceStandard: ENV_VARS.forceStandard.parse(
getFirstDefined([
ENV_VARS.forceStandard.name,
...(ENV_VARS.forceStandard.aliases ?? []),
]),
),
skipInstall: ENV_VARS.skipInstall.parse(
getFirstDefined([
ENV_VARS.skipInstall.name,
...(ENV_VARS.skipInstall.aliases ?? []),
]),
),
binPath: ENV_VARS.binPath.parse(
getFirstDefined([
ENV_VARS.binPath.name,
...(ENV_VARS.binPath.aliases ?? []),
]),
),
downloadBaseUrl: ENV_VARS.downloadBaseUrl.parse(
getFirstDefined([
ENV_VARS.downloadBaseUrl.name,
...(ENV_VARS.downloadBaseUrl.aliases ?? []),
]),
),
skipChecksum: ENV_VARS.skipChecksum.parse(
getFirstDefined([
ENV_VARS.skipChecksum.name,
...(ENV_VARS.skipChecksum.aliases ?? []),
]),
),
quiet: ENV_VARS.quiet.parse(
getFirstDefined([ENV_VARS.quiet.name, ...(ENV_VARS.quiet.aliases ?? [])]),
),
};
}
/**
* Metadata about all supported environment variables.
* Useful for documentation generation or help output.
*/
export const ENV_VAR_DOCS = Object.entries(ENV_VARS).map(([key, config]) => ({
key,
name: config.name,
aliases: config.aliases ?? [],
description: config.description,
type:
config.defaultValue === undefined
? "string"
: typeof config.defaultValue === "boolean"
? "boolean"
: "string",
default: config.defaultValue,
}));
+41 -18
View File
@@ -7,6 +7,7 @@ import { pipeline } from "node:stream/promises";
import { fileURLToPath } from "node:url";
import AdmZip from "adm-zip";
import * as tar from "tar";
import { getEnvConfig } from "./env";
import {
getBinFilename,
getBinVersion,
@@ -15,6 +16,7 @@ import {
getReleaseFilename,
getReleaseUrl,
isExtended,
logger,
} from "./utils";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -129,18 +131,27 @@ async function verifyChecksum(
*
* This function handles the complete installation process:
* - Determines the correct Hugo release file for the current platform and architecture
* - Downloads the release file and checksums from GitHub
* - Verifies the integrity of the downloaded file using SHA-256 checksums
* - Downloads the release file and checksums from GitHub (or custom mirror)
* - Verifies the integrity of the downloaded file using SHA-256 checksums (unless HUGO_SKIP_CHECKSUM is set)
* - Extracts or installs the binary (platform-specific):
* - macOS: Uses `sudo installer` to install the .pkg file to /usr/local/bin
* - Windows/Linux: Extracts the .zip or .tar.gz archive to the local bin directory
* - Sets appropriate file permissions on Unix-like systems
* - Displays the installed Hugo version
*
* Environment variables that affect installation:
* - HUGO_OVERRIDE_VERSION: Install a different Hugo version
* - HUGO_NO_EXTENDED: Force vanilla Hugo instead of Extended
* - HUGO_MIRROR_BASE_URL: Custom download mirror
* - HUGO_SKIP_CHECKSUM: Skip SHA-256 verification
* - HUGO_QUIET: Suppress progress output
*
* @throws {Error} If the platform is unsupported, download fails, checksum doesn't match, or installation fails
* @returns A promise that resolves with the absolute path to the installed Hugo binary
*/
async function install(): Promise<string> {
const envConfig = getEnvConfig();
try {
const version = getPkgVersion();
const releaseFile = getReleaseFilename(version);
@@ -154,9 +165,13 @@ async function install(): Promise<string> {
}
if (!isExtended(releaseFile)) {
console.warn(
"️ Hugo Extended isn't supported on this platform, downloading vanilla Hugo instead.",
);
if (envConfig.forceStandard) {
logger.info("️ Installing vanilla Hugo (HUGO_NO_EXTENDED is set).");
} else {
logger.warn(
"️ Hugo Extended isn't supported on this platform, downloading vanilla Hugo instead.",
);
}
}
// Prepare bin directory
@@ -169,20 +184,29 @@ async function install(): Promise<string> {
const checksumUrl = getReleaseUrl(version, checksumFile);
const downloadPath = path.join(binDir, releaseFile);
console.info(`☁️ Downloading ${releaseFile}...`);
logger.info(`☁️ Downloading ${releaseFile}...`);
await downloadFile(releaseUrl, downloadPath);
console.info("🕵️ Verifying checksum...");
await verifyChecksum(downloadPath, checksumUrl, releaseFile);
if (envConfig.skipChecksum) {
logger.warn(
"⚠️ Skipping checksum verification (HUGO_SKIP_CHECKSUM is set).",
);
} else {
logger.info("🕵️ Verifying checksum...");
await verifyChecksum(downloadPath, checksumUrl, releaseFile);
}
if (process.platform === "darwin") {
console.info(`💾 Installing ${releaseFile} (requires sudo)...`);
const archiveType = getArchiveType(releaseFile);
// macOS .pkg files require special handling with sudo installer
if (process.platform === "darwin" && archiveType === "pkg") {
logger.info(`💾 Installing ${releaseFile} (requires sudo)...`);
// Run MacOS installer
const result = spawnSync(
"sudo",
["installer", "-pkg", downloadPath, "-target", "/"],
{
stdio: "inherit",
stdio: envConfig.quiet ? "pipe" : "inherit",
},
);
@@ -204,9 +228,9 @@ async function install(): Promise<string> {
}
fs.symlinkSync("/usr/local/bin/hugo", symlinkPath);
} else {
console.info("📦 Extracting...");
// All other platforms and macOS pre-0.153.0 (tar.gz) use archive extraction
logger.info("📦 Extracting...");
const archiveType = getArchiveType(releaseFile);
if (archiveType === "zip") {
const zip = new AdmZip(downloadPath);
zip.extractAllTo(binDir, true);
@@ -223,9 +247,8 @@ async function install(): Promise<string> {
fs.unlinkSync(downloadPath);
} else {
// Defensive: should not happen since unsupported platforms are caught earlier
// and pkg files are handled in the darwin branch above
throw new Error(
`Unexpected archive type for ${releaseFile}. Expected .zip or .tar.gz for this platform.`,
`Unexpected archive type for ${releaseFile}. Expected .zip, .tar.gz, or .pkg.`,
);
}
@@ -235,14 +258,14 @@ async function install(): Promise<string> {
}
}
console.info("🎉 Hugo installed successfully!");
logger.info("🎉 Hugo installed successfully!");
// Check version and return path
const binPath = path.join(binDir, binFile);
console.info(getBinVersion(binPath));
logger.info(getBinVersion(binPath));
return binPath;
} catch (error) {
console.error("⛔ Hugo installation failed. :(");
logger.error("⛔ Hugo installation failed. :(");
throw error;
}
}
+163 -32
View File
@@ -2,20 +2,71 @@ import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { getEnvConfig } from "./env";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/**
* Gets the Hugo version to install from package.json.
* The first Hugo version that uses .pkg installers for macOS.
* Versions before this use .tar.gz archives.
*
* This package's version number (should) always match the Hugo release we want.
* We check for a `hugoVersion` field in package.json just in case it doesn't
* match in the future (from pushing an emergency package update, etc.).
* @see https://github.com/gohugoio/hugo/issues/14135
*/
const MACOS_PKG_MIN_VERSION = "0.153.0";
/**
* Compares two semver version strings.
*
* @throws {Error} If package.json cannot be found
* @param a - First version string (e.g., "0.153.0")
* @param b - Second version string (e.g., "0.152.1")
* @returns -1 if a < b, 0 if a === b, 1 if a > b
*/
export function compareVersions(a: string, b: string): -1 | 0 | 1 {
const partsA = a.split(".").map((n) => Number.parseInt(n, 10));
const partsB = b.split(".").map((n) => Number.parseInt(n, 10));
const maxLen = Math.max(partsA.length, partsB.length);
for (let i = 0; i < maxLen; i++) {
const numA = partsA[i] ?? 0;
const numB = partsB[i] ?? 0;
if (numA < numB) return -1;
if (numA > numB) return 1;
}
return 0;
}
/**
* Checks if a version uses .pkg installers for macOS.
* Hugo v0.153.0+ uses .pkg, earlier versions use .tar.gz.
*
* @param version - The Hugo version to check
* @returns true if the version uses .pkg installers on macOS
*/
export function usesMacOSPkg(version: string): boolean {
return compareVersions(version, MACOS_PKG_MIN_VERSION) >= 0;
}
/**
* Gets the Hugo version to install.
*
* Resolution order:
* 1. HUGO_OVERRIDE_VERSION environment variable (if set)
* 2. `hugoVersion` field in package.json (for emergency overrides)
* 3. `version` field in package.json (should match Hugo release)
*
* @throws {Error} If package.json cannot be found and no override is set
* @returns The version string (e.g., "0.88.1")
*/
export function getPkgVersion(): string {
// Check for environment variable override first
const envConfig = getEnvConfig();
if (envConfig.overrideVersion) {
return envConfig.overrideVersion;
}
// Walk up from __dirname (dist/lib) to find package.json
const packageJsonPath = path.join(__dirname, "..", "..", "package.json");
@@ -33,13 +84,23 @@ export function getPkgVersion(): string {
}
/**
* Generates the full GitHub URL to a Hugo release file.
* Generates the full URL to a Hugo release file.
*
* By default, downloads from GitHub releases. Can be overridden with
* HUGO_MIRROR_BASE_URL for mirrors or air-gapped environments.
*
* @param version - The Hugo version number (e.g., "0.88.1")
* @param filename - The release filename (e.g., "hugo_extended_0.88.1_darwin-universal.pkg")
* @returns The complete download URL for the release file
*/
export function getReleaseUrl(version: string, filename: string): string {
const envConfig = getEnvConfig();
if (envConfig.downloadBaseUrl) {
// Custom mirror: append filename to base URL
const baseUrl = envConfig.downloadBaseUrl.replace(/\/$/, "");
return `${baseUrl}/${filename}`;
}
// Default: GitHub releases
return `https://github.com/gohugoio/hugo/releases/download/v${version}/${filename}`;
}
@@ -53,12 +114,20 @@ export function getBinFilename(): string {
}
/**
* Gets the absolute path to the installed Hugo binary.
* Gets the absolute path to the Hugo binary.
*
* @returns The absolute path to hugo binary in the local bin directory.
* On macOS, this is a symlink to "/usr/local/bin/hugo".
* Resolution order:
* 1. HUGO_BIN_PATH environment variable (if set)
* 2. Local bin directory (./bin/hugo or ./bin/hugo.exe)
*
* @returns The absolute path to hugo binary.
* On macOS (when using local bin), this is a symlink to "/usr/local/bin/hugo".
*/
export function getBinPath(): string {
const envConfig = getEnvConfig();
if (envConfig.binPath) {
return envConfig.binPath;
}
return path.join(__dirname, "..", "..", "bin", getBinFilename());
}
@@ -111,6 +180,9 @@ export function doesBinExist(bin: string): boolean {
* - Windows: x64 only
*
* Other platform/architecture combinations fall back to vanilla Hugo where available.
* Set HUGO_NO_EXTENDED=1 to force vanilla Hugo even on platforms that support Extended.
*
* Note: macOS uses .pkg installers starting from v0.153.0. Earlier versions use .tar.gz.
*
* @param version - The Hugo version number (e.g., "0.88.1")
* @returns The release filename if supported (e.g., "hugo_extended_0.88.1_darwin-universal.pkg"),
@@ -118,31 +190,60 @@ export function doesBinExist(bin: string): boolean {
*/
export function getReleaseFilename(version: string): string | null {
const { platform, arch } = process;
const envConfig = getEnvConfig();
const forceStandard = envConfig.forceStandard;
// Helper to choose between extended and standard edition
const edition = (extended: string, standard: string): string =>
forceStandard ? standard : extended;
// macOS: as of 0.102.0, binaries are universal
// As of v0.153.0, macOS uses .pkg installers instead of .tar.gz
if (platform === "darwin" && (arch === "x64" || arch === "arm64")) {
if (usesMacOSPkg(version)) {
// v0.153.0+: .pkg installer
return edition(
`hugo_extended_${version}_darwin-universal.pkg`,
`hugo_${version}_darwin-universal.pkg`,
);
}
// Pre-v0.153.0: .tar.gz archive
return edition(
`hugo_extended_${version}_darwin-universal.tar.gz`,
`hugo_${version}_darwin-universal.tar.gz`,
);
}
const filename =
// macOS: as of 0.102.0, binaries are universal
platform === "darwin" && arch === "x64"
? `hugo_extended_${version}_darwin-universal.pkg`
: platform === "darwin" && arch === "arm64"
? `hugo_extended_${version}_darwin-universal.pkg`
: // Windows
platform === "win32" && arch === "x64"
? `hugo_extended_${version}_windows-amd64.zip`
: platform === "win32" && arch === "arm64"
? `hugo_${version}_windows-arm64.zip`
: // Linux
platform === "linux" && arch === "x64"
? `hugo_extended_${version}_linux-amd64.tar.gz`
: platform === "linux" && arch === "arm64"
? `hugo_extended_${version}_linux-arm64.tar.gz`
: // FreeBSD
platform === "freebsd" && arch === "x64"
? `hugo_${version}_freebsd-amd64.tar.gz`
: // OpenBSD
platform === "openbsd" && arch === "x64"
? `hugo_${version}_openbsd-amd64.tar.gz`
: // not gonna work :(
null;
// Windows x64: Extended available
platform === "win32" && arch === "x64"
? edition(
`hugo_extended_${version}_windows-amd64.zip`,
`hugo_${version}_windows-amd64.zip`,
)
: // Windows ARM64: Extended not available
platform === "win32" && arch === "arm64"
? `hugo_${version}_windows-arm64.zip`
: // Linux x64: Extended available
platform === "linux" && arch === "x64"
? edition(
`hugo_extended_${version}_linux-amd64.tar.gz`,
`hugo_${version}_linux-amd64.tar.gz`,
)
: // Linux ARM64: Extended available
platform === "linux" && arch === "arm64"
? edition(
`hugo_extended_${version}_linux-arm64.tar.gz`,
`hugo_${version}_linux-arm64.tar.gz`,
)
: // FreeBSD: Extended not available
platform === "freebsd" && arch === "x64"
? `hugo_${version}_freebsd-amd64.tar.gz`
: // OpenBSD: Extended not available
platform === "openbsd" && arch === "x64"
? `hugo_${version}_openbsd-amd64.tar.gz`
: // not gonna work :(
null;
return filename;
}
@@ -166,3 +267,33 @@ export function getChecksumFilename(version: string): string {
export function isExtended(releaseFile: string): boolean {
return releaseFile.startsWith("hugo_extended_");
}
/**
* Logger utility that respects the HUGO_QUIET setting.
*/
export const logger = {
/**
* Log an info message (respects HUGO_QUIET).
*/
info: (message: string): void => {
if (!getEnvConfig().quiet) {
console.info(message);
}
},
/**
* Log a warning message (respects HUGO_QUIET).
*/
warn: (message: string): void => {
if (!getEnvConfig().quiet) {
console.warn(message);
}
},
/**
* Log an error message (always shown, even in quiet mode).
*/
error: (message: string): void => {
console.error(message);
},
};
+239
View File
@@ -0,0 +1,239 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { ENV_VAR_DOCS, getEnvConfig } from "../../src/lib/env";
describe("env", () => {
// Store original env vars to restore after each test
const originalEnv: Record<string, string | undefined> = {};
// List of all env vars we might set during tests
const envVars = [
"HUGO_OVERRIDE_VERSION",
"HUGO_NO_EXTENDED",
"HUGO_FORCE_STANDARD",
"HUGO_SKIP_DOWNLOAD",
"HUGO_BIN_PATH",
"HUGO_MIRROR_BASE_URL",
"HUGO_SKIP_CHECKSUM",
"HUGO_SKIP_VERIFY",
"HUGO_QUIET",
"HUGO_SILENT",
];
beforeEach(() => {
// Save original values
for (const key of envVars) {
originalEnv[key] = process.env[key];
delete process.env[key];
}
});
afterEach(() => {
// Restore original values
for (const key of envVars) {
if (originalEnv[key] === undefined) {
delete process.env[key];
} else {
process.env[key] = originalEnv[key];
}
}
});
describe("getEnvConfig", () => {
describe("overrideVersion", () => {
it("should return undefined when not set", () => {
const config = getEnvConfig();
expect(config.overrideVersion).toBeUndefined();
});
it("should return the value from HUGO_OVERRIDE_VERSION", () => {
process.env.HUGO_OVERRIDE_VERSION = "0.139.0";
const config = getEnvConfig();
expect(config.overrideVersion).toBe("0.139.0");
});
it("should strip v prefix from version", () => {
process.env.HUGO_OVERRIDE_VERSION = "v0.139.0";
const config = getEnvConfig();
expect(config.overrideVersion).toBe("0.139.0");
});
it("should return undefined for empty string", () => {
process.env.HUGO_OVERRIDE_VERSION = "";
const config = getEnvConfig();
expect(config.overrideVersion).toBeUndefined();
});
it("should return undefined for whitespace-only string", () => {
process.env.HUGO_OVERRIDE_VERSION = " ";
const config = getEnvConfig();
expect(config.overrideVersion).toBeUndefined();
});
});
describe("forceStandard", () => {
it("should return false when not set", () => {
const config = getEnvConfig();
expect(config.forceStandard).toBe(false);
});
it.each([
"1",
"true",
"yes",
"on",
"TRUE",
"True",
"YES",
"ON",
])('should return true for "%s"', (value) => {
process.env.HUGO_NO_EXTENDED = value;
const config = getEnvConfig();
expect(config.forceStandard).toBe(true);
});
it.each([
"0",
"false",
"no",
"off",
"",
"invalid",
])('should return false for "%s"', (value) => {
process.env.HUGO_NO_EXTENDED = value;
const config = getEnvConfig();
expect(config.forceStandard).toBe(false);
});
it("should work with HUGO_FORCE_STANDARD alias", () => {
process.env.HUGO_FORCE_STANDARD = "1";
const config = getEnvConfig();
expect(config.forceStandard).toBe(true);
});
});
describe("skipInstall", () => {
it("should return false when not set", () => {
const config = getEnvConfig();
expect(config.skipInstall).toBe(false);
});
it("should return true when HUGO_SKIP_DOWNLOAD is truthy", () => {
process.env.HUGO_SKIP_DOWNLOAD = "1";
const config = getEnvConfig();
expect(config.skipInstall).toBe(true);
});
it("should work with HUGO_SKIP_DOWNLOAD alias", () => {
process.env.HUGO_SKIP_DOWNLOAD = "true";
const config = getEnvConfig();
expect(config.skipInstall).toBe(true);
});
});
describe("binPath", () => {
it("should return undefined when not set", () => {
const config = getEnvConfig();
expect(config.binPath).toBeUndefined();
});
it("should return the value from HUGO_BIN_PATH", () => {
process.env.HUGO_BIN_PATH = "/usr/local/bin/hugo";
const config = getEnvConfig();
expect(config.binPath).toBe("/usr/local/bin/hugo");
});
it("should trim whitespace", () => {
process.env.HUGO_BIN_PATH = " /usr/local/bin/hugo ";
const config = getEnvConfig();
expect(config.binPath).toBe("/usr/local/bin/hugo");
});
});
describe("downloadBaseUrl", () => {
it("should return undefined when not set", () => {
const config = getEnvConfig();
expect(config.downloadBaseUrl).toBeUndefined();
});
it("should return the value from HUGO_MIRROR_BASE_URL", () => {
process.env.HUGO_MIRROR_BASE_URL = "https://mirror.example.com/hugo";
const config = getEnvConfig();
expect(config.downloadBaseUrl).toBe("https://mirror.example.com/hugo");
});
});
describe("skipChecksum", () => {
it("should return false when not set", () => {
const config = getEnvConfig();
expect(config.skipChecksum).toBe(false);
});
it("should return true when HUGO_SKIP_CHECKSUM is truthy", () => {
process.env.HUGO_SKIP_CHECKSUM = "1";
const config = getEnvConfig();
expect(config.skipChecksum).toBe(true);
});
it("should work with HUGO_SKIP_VERIFY alias", () => {
process.env.HUGO_SKIP_VERIFY = "true";
const config = getEnvConfig();
expect(config.skipChecksum).toBe(true);
});
});
describe("quiet", () => {
it("should return false when not set", () => {
const config = getEnvConfig();
expect(config.quiet).toBe(false);
});
it("should return true when HUGO_QUIET is truthy", () => {
process.env.HUGO_QUIET = "1";
const config = getEnvConfig();
expect(config.quiet).toBe(true);
});
it("should work with HUGO_SILENT alias", () => {
process.env.HUGO_SILENT = "true";
const config = getEnvConfig();
expect(config.quiet).toBe(true);
});
});
});
describe("ENV_VAR_DOCS", () => {
it("should export documentation for all env vars", () => {
expect(ENV_VAR_DOCS).toBeDefined();
expect(Array.isArray(ENV_VAR_DOCS)).toBe(true);
expect(ENV_VAR_DOCS.length).toBeGreaterThan(0);
});
it("should have required fields for each entry", () => {
for (const doc of ENV_VAR_DOCS) {
expect(doc.key).toBeDefined();
expect(doc.name).toBeDefined();
expect(doc.name).toMatch(/^HUGO_/);
expect(doc.description).toBeDefined();
expect(Array.isArray(doc.aliases)).toBe(true);
expect(["string", "boolean"]).toContain(doc.type);
}
});
it("should document all expected env vars", () => {
const expectedKeys = [
"overrideVersion",
"forceStandard",
"skipInstall",
"binPath",
"downloadBaseUrl",
"skipChecksum",
"quiet",
];
const actualKeys = ENV_VAR_DOCS.map((doc) => doc.key);
for (const key of expectedKeys) {
expect(actualKeys).toContain(key);
}
});
});
});
+277 -3
View File
@@ -1,13 +1,58 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
compareVersions,
getBinFilename,
getBinPath,
getChecksumFilename,
getReleaseFilename,
getReleaseUrl,
isExtended,
logger,
usesMacOSPkg,
} from "../../src/lib/utils";
describe("utils", () => {
describe("compareVersions", () => {
it("should return 0 for equal versions", () => {
expect(compareVersions("0.153.0", "0.153.0")).toBe(0);
expect(compareVersions("1.0.0", "1.0.0")).toBe(0);
});
it("should return -1 when first version is less", () => {
expect(compareVersions("0.152.0", "0.153.0")).toBe(-1);
expect(compareVersions("0.152.9", "0.153.0")).toBe(-1);
expect(compareVersions("0.100.0", "0.153.0")).toBe(-1);
expect(compareVersions("0.153.0", "1.0.0")).toBe(-1);
});
it("should return 1 when first version is greater", () => {
expect(compareVersions("0.154.0", "0.153.0")).toBe(1);
expect(compareVersions("0.153.1", "0.153.0")).toBe(1);
expect(compareVersions("1.0.0", "0.153.0")).toBe(1);
});
it("should handle versions with different segment counts", () => {
expect(compareVersions("0.153", "0.153.0")).toBe(0);
expect(compareVersions("0.153.0.1", "0.153.0")).toBe(1);
});
});
describe("usesMacOSPkg", () => {
it("should return true for v0.153.0 and later", () => {
expect(usesMacOSPkg("0.153.0")).toBe(true);
expect(usesMacOSPkg("0.154.0")).toBe(true);
expect(usesMacOSPkg("0.154.3")).toBe(true);
expect(usesMacOSPkg("1.0.0")).toBe(true);
});
it("should return false for versions before v0.153.0", () => {
expect(usesMacOSPkg("0.152.0")).toBe(false);
expect(usesMacOSPkg("0.152.9")).toBe(false);
expect(usesMacOSPkg("0.100.0")).toBe(false);
expect(usesMacOSPkg("0.102.3")).toBe(false);
});
});
describe("getBinFilename", () => {
let originalPlatform: NodeJS.Platform;
@@ -55,7 +100,7 @@ describe("utils", () => {
});
describe("macOS", () => {
it("should return universal pkg for darwin x64", () => {
it("should return universal pkg for darwin x64 (v0.153.0+)", () => {
Object.defineProperty(process, "platform", { value: "darwin" });
Object.defineProperty(process, "arch", { value: "x64" });
expect(getReleaseFilename("0.154.3")).toBe(
@@ -63,13 +108,37 @@ describe("utils", () => {
);
});
it("should return universal pkg for darwin arm64", () => {
it("should return universal pkg for darwin arm64 (v0.153.0+)", () => {
Object.defineProperty(process, "platform", { value: "darwin" });
Object.defineProperty(process, "arch", { value: "arm64" });
expect(getReleaseFilename("0.154.3")).toBe(
"hugo_extended_0.154.3_darwin-universal.pkg",
);
});
it("should return universal tar.gz for darwin x64 (pre-v0.153.0)", () => {
Object.defineProperty(process, "platform", { value: "darwin" });
Object.defineProperty(process, "arch", { value: "x64" });
expect(getReleaseFilename("0.152.0")).toBe(
"hugo_extended_0.152.0_darwin-universal.tar.gz",
);
});
it("should return universal tar.gz for darwin arm64 (pre-v0.153.0)", () => {
Object.defineProperty(process, "platform", { value: "darwin" });
Object.defineProperty(process, "arch", { value: "arm64" });
expect(getReleaseFilename("0.139.0")).toBe(
"hugo_extended_0.139.0_darwin-universal.tar.gz",
);
});
it("should return pkg at version boundary (v0.153.0)", () => {
Object.defineProperty(process, "platform", { value: "darwin" });
Object.defineProperty(process, "arch", { value: "arm64" });
expect(getReleaseFilename("0.153.0")).toBe(
"hugo_extended_0.153.0_darwin-universal.pkg",
);
});
});
describe("Windows", () => {
@@ -175,6 +244,21 @@ describe("utils", () => {
});
describe("getReleaseUrl", () => {
let originalMirrorBaseUrl: string | undefined;
beforeEach(() => {
originalMirrorBaseUrl = process.env.HUGO_MIRROR_BASE_URL;
delete process.env.HUGO_MIRROR_BASE_URL;
});
afterEach(() => {
if (originalMirrorBaseUrl === undefined) {
delete process.env.HUGO_MIRROR_BASE_URL;
} else {
process.env.HUGO_MIRROR_BASE_URL = originalMirrorBaseUrl;
}
});
it("should return correct GitHub release URL", () => {
expect(
getReleaseUrl("0.154.3", "hugo_extended_0.154.3_linux-amd64.tar.gz"),
@@ -188,5 +272,195 @@ describe("utils", () => {
"https://github.com/gohugoio/hugo/releases/download/v0.154.3/hugo_0.154.3_checksums.txt",
);
});
it("should use custom base URL when HUGO_MIRROR_BASE_URL is set", () => {
process.env.HUGO_MIRROR_BASE_URL = "https://mirror.example.com/hugo";
expect(
getReleaseUrl("0.154.3", "hugo_extended_0.154.3_linux-amd64.tar.gz"),
).toBe(
"https://mirror.example.com/hugo/hugo_extended_0.154.3_linux-amd64.tar.gz",
);
});
it("should strip trailing slash from custom base URL", () => {
process.env.HUGO_MIRROR_BASE_URL = "https://mirror.example.com/hugo/";
expect(
getReleaseUrl("0.154.3", "hugo_extended_0.154.3_linux-amd64.tar.gz"),
).toBe(
"https://mirror.example.com/hugo/hugo_extended_0.154.3_linux-amd64.tar.gz",
);
});
});
describe("getReleaseFilename with HUGO_NO_EXTENDED", () => {
let originalPlatform: NodeJS.Platform;
let originalArch: NodeJS.Architecture;
let originalNoExtended: string | undefined;
let originalForceStandard: string | undefined;
beforeEach(() => {
originalPlatform = process.platform;
originalArch = process.arch;
originalNoExtended = process.env.HUGO_NO_EXTENDED;
originalForceStandard = process.env.HUGO_FORCE_STANDARD;
delete process.env.HUGO_NO_EXTENDED;
delete process.env.HUGO_FORCE_STANDARD;
});
afterEach(() => {
Object.defineProperty(process, "platform", { value: originalPlatform });
Object.defineProperty(process, "arch", { value: originalArch });
if (originalNoExtended === undefined) {
delete process.env.HUGO_NO_EXTENDED;
} else {
process.env.HUGO_NO_EXTENDED = originalNoExtended;
}
if (originalForceStandard === undefined) {
delete process.env.HUGO_FORCE_STANDARD;
} else {
process.env.HUGO_FORCE_STANDARD = originalForceStandard;
}
});
it("should return vanilla Hugo pkg when HUGO_NO_EXTENDED is set on macOS (v0.153.0+)", () => {
Object.defineProperty(process, "platform", { value: "darwin" });
Object.defineProperty(process, "arch", { value: "arm64" });
process.env.HUGO_NO_EXTENDED = "1";
expect(getReleaseFilename("0.154.3")).toBe(
"hugo_0.154.3_darwin-universal.pkg",
);
});
it("should return vanilla Hugo tar.gz when HUGO_NO_EXTENDED is set on macOS (pre-v0.153.0)", () => {
Object.defineProperty(process, "platform", { value: "darwin" });
Object.defineProperty(process, "arch", { value: "arm64" });
process.env.HUGO_NO_EXTENDED = "1";
expect(getReleaseFilename("0.152.0")).toBe(
"hugo_0.152.0_darwin-universal.tar.gz",
);
});
it("should return vanilla Hugo when HUGO_NO_EXTENDED is set on Linux", () => {
Object.defineProperty(process, "platform", { value: "linux" });
Object.defineProperty(process, "arch", { value: "x64" });
process.env.HUGO_NO_EXTENDED = "1";
expect(getReleaseFilename("0.154.3")).toBe(
"hugo_0.154.3_linux-amd64.tar.gz",
);
});
it("should return vanilla Hugo when HUGO_NO_EXTENDED is set on Windows", () => {
Object.defineProperty(process, "platform", { value: "win32" });
Object.defineProperty(process, "arch", { value: "x64" });
process.env.HUGO_NO_EXTENDED = "1";
expect(getReleaseFilename("0.154.3")).toBe(
"hugo_0.154.3_windows-amd64.zip",
);
});
it("should work with HUGO_FORCE_STANDARD alias", () => {
Object.defineProperty(process, "platform", { value: "linux" });
Object.defineProperty(process, "arch", { value: "arm64" });
process.env.HUGO_FORCE_STANDARD = "true";
expect(getReleaseFilename("0.154.3")).toBe(
"hugo_0.154.3_linux-arm64.tar.gz",
);
});
});
describe("getBinPath with HUGO_BIN_PATH", () => {
let originalBinPath: string | undefined;
beforeEach(() => {
originalBinPath = process.env.HUGO_BIN_PATH;
delete process.env.HUGO_BIN_PATH;
});
afterEach(() => {
if (originalBinPath === undefined) {
delete process.env.HUGO_BIN_PATH;
} else {
process.env.HUGO_BIN_PATH = originalBinPath;
}
});
it("should return custom path when HUGO_BIN_PATH is set", () => {
process.env.HUGO_BIN_PATH = "/custom/path/to/hugo";
expect(getBinPath()).toBe("/custom/path/to/hugo");
});
it("should return default path when HUGO_BIN_PATH is not set", () => {
const binPath = getBinPath();
expect(binPath).toContain("bin");
expect(binPath).toMatch(/hugo(\.exe)?$/);
});
});
describe("logger", () => {
let originalQuiet: string | undefined;
let originalSilent: string | undefined;
beforeEach(() => {
originalQuiet = process.env.HUGO_QUIET;
originalSilent = process.env.HUGO_SILENT;
delete process.env.HUGO_QUIET;
delete process.env.HUGO_SILENT;
vi.spyOn(console, "info").mockImplementation(() => {});
vi.spyOn(console, "warn").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
if (originalQuiet === undefined) {
delete process.env.HUGO_QUIET;
} else {
process.env.HUGO_QUIET = originalQuiet;
}
if (originalSilent === undefined) {
delete process.env.HUGO_SILENT;
} else {
process.env.HUGO_SILENT = originalSilent;
}
vi.restoreAllMocks();
});
describe("info", () => {
it("should log when not quiet", () => {
logger.info("test message");
expect(console.info).toHaveBeenCalledWith("test message");
});
it("should not log when HUGO_QUIET is set", () => {
process.env.HUGO_QUIET = "1";
logger.info("test message");
expect(console.info).not.toHaveBeenCalled();
});
});
describe("warn", () => {
it("should log when not quiet", () => {
logger.warn("warning message");
expect(console.warn).toHaveBeenCalledWith("warning message");
});
it("should not log when HUGO_SILENT is set", () => {
process.env.HUGO_SILENT = "1";
logger.warn("warning message");
expect(console.warn).not.toHaveBeenCalled();
});
});
describe("error", () => {
it("should always log errors", () => {
logger.error("error message");
expect(console.error).toHaveBeenCalledWith("error message");
});
it("should log errors even when quiet", () => {
process.env.HUGO_QUIET = "1";
logger.error("error message");
expect(console.error).toHaveBeenCalledWith("error message");
});
});
});
});