mirror of
https://github.com/jakejarvis/hugo-extended.git
synced 2026-07-29 19:55:21 -04:00
refactor: replace postinstall script with platform-specific binary packages
This commit is contained in:
@@ -1,10 +0,0 @@
|
||||
# http://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
@@ -1,2 +0,0 @@
|
||||
# Set default behavior to automatically normalize line endings
|
||||
* text=auto eol=lf
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
LATEST="${{ steps.hugo.outputs.version }}"
|
||||
BRANCH_NAME="autobump-hugo-v${LATEST}"
|
||||
BRANCH_NAME="chore/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')
|
||||
@@ -89,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="autobump-hugo-v${LATEST}"
|
||||
BRANCH_NAME="chore/hugo-v${LATEST}"
|
||||
|
||||
# Create new branch
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
@@ -97,6 +97,8 @@ jobs:
|
||||
# Use npm version to properly bump (updates package.json and package-lock.json)
|
||||
# --no-git-tag-version prevents creating a tag, we'll let the publish workflow handle that
|
||||
npm version "$LATEST" --no-git-tag-version
|
||||
npm run sync-binary-package-versions
|
||||
npm install --package-lock-only --ignore-scripts
|
||||
|
||||
# Stage package files
|
||||
git add package.json package-lock.json
|
||||
@@ -105,7 +107,7 @@ jobs:
|
||||
if: steps.check.outputs.needs_update == 'true' && steps.pr_check.outputs.pr_exists == 'false'
|
||||
run: |
|
||||
LATEST="${{ steps.hugo.outputs.version }}"
|
||||
BRANCH_NAME="autobump-hugo-v${LATEST}"
|
||||
BRANCH_NAME="chore/hugo-v${LATEST}"
|
||||
|
||||
git commit -m "chore: bump to Hugo v${LATEST}"
|
||||
git push origin "$BRANCH_NAME"
|
||||
@@ -116,7 +118,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
LATEST="${{ steps.hugo.outputs.version }}"
|
||||
BRANCH_NAME="autobump-hugo-v${LATEST}"
|
||||
BRANCH_NAME="chore/hugo-v${LATEST}"
|
||||
|
||||
gh pr create \
|
||||
--title "chore: bump to Hugo v${LATEST}" \
|
||||
@@ -10,8 +10,54 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
npm:
|
||||
name: Publish to NPM
|
||||
binary-packages:
|
||||
name: Publish ${{ matrix.package_name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- package_name: "@jakejarvis/hugo-extended-darwin-universal"
|
||||
os: macos-latest
|
||||
- package_name: "@jakejarvis/hugo-extended-linux-amd64"
|
||||
os: ubuntu-latest
|
||||
- package_name: "@jakejarvis/hugo-extended-linux-arm64"
|
||||
os: ubuntu-latest
|
||||
- package_name: "@jakejarvis/hugo-extended-windows-amd64"
|
||||
os: ubuntu-latest
|
||||
- package_name: "@jakejarvis/hugo-windows-arm64"
|
||||
os: ubuntu-latest
|
||||
steps:
|
||||
- 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: Install dependencies
|
||||
run: npm install -g npm@latest && npm ci
|
||||
|
||||
- name: Generate binary package
|
||||
env:
|
||||
PACKAGE_NAME: ${{ matrix.package_name }}
|
||||
run: npm run generate-binary-packages -- --package "$PACKAGE_NAME" --pack-dry-run
|
||||
|
||||
- name: Publish binary package
|
||||
env:
|
||||
PACKAGE_NAME: ${{ matrix.package_name }}
|
||||
shell: bash
|
||||
run: |
|
||||
package_dir="dist-platforms/${PACKAGE_NAME#@jakejarvis/}"
|
||||
npm publish "$package_dir" --access public
|
||||
|
||||
main-package:
|
||||
name: Publish hugo-extended
|
||||
needs: binary-packages
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -25,12 +71,12 @@ jobs:
|
||||
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 current platform binary package
|
||||
run: npm run generate-binary-packages -- --set-github-env
|
||||
|
||||
- name: Generate types
|
||||
run: npm run generate-types
|
||||
|
||||
@@ -38,7 +84,7 @@ jobs:
|
||||
run: npm run lint
|
||||
|
||||
- name: Type check
|
||||
run: npm run typecheck
|
||||
run: npm run check-types
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
+43
-317
@@ -10,7 +10,6 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
# Fast unit tests - run first to catch basic issues quickly
|
||||
unit:
|
||||
name: Unit Tests (Node ${{ matrix.node }})
|
||||
runs-on: ubuntu-latest
|
||||
@@ -28,12 +27,12 @@ jobs:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Generate current platform binary package
|
||||
run: npm run generate-binary-packages -- --set-github-env
|
||||
|
||||
- name: Generate types
|
||||
run: npm run generate-types
|
||||
|
||||
@@ -41,7 +40,7 @@ jobs:
|
||||
run: npm run lint
|
||||
|
||||
- name: Type check
|
||||
run: npm run typecheck
|
||||
run: npm run check-types
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
@@ -49,7 +48,6 @@ jobs:
|
||||
- name: Run unit tests
|
||||
run: npm run test:unit
|
||||
|
||||
# Integration tests - run Hugo commands on each platform
|
||||
integration:
|
||||
name: Integration (${{ matrix.os }}, Node ${{ matrix.node }})
|
||||
needs: unit
|
||||
@@ -69,12 +67,12 @@ jobs:
|
||||
node-version: ${{ matrix.node }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Generate current platform binary package
|
||||
run: npm run generate-binary-packages -- --set-github-env
|
||||
|
||||
- name: Generate types
|
||||
run: npm run generate-types
|
||||
|
||||
@@ -84,9 +82,8 @@ jobs:
|
||||
- name: Run integration tests
|
||||
run: npm run test:integration
|
||||
|
||||
# E2E installation tests - verify the full installation pipeline
|
||||
e2e:
|
||||
name: E2E Installation (${{ matrix.os }})
|
||||
name: E2E Binary Package (${{ matrix.os }})
|
||||
needs: unit
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -103,12 +100,12 @@ jobs:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Generate current platform binary package
|
||||
run: npm run generate-binary-packages -- --set-github-env
|
||||
|
||||
- name: Generate types
|
||||
run: npm run generate-types
|
||||
|
||||
@@ -118,9 +115,8 @@ jobs:
|
||||
- name: Run E2E tests
|
||||
run: npm run test:e2e
|
||||
|
||||
# Fresh installation test - simulates a user installing the package
|
||||
fresh-install:
|
||||
name: Fresh Install (${{ matrix.os }})
|
||||
package-resolution:
|
||||
name: Package Resolution (${{ matrix.os }})
|
||||
needs: unit
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -137,147 +133,40 @@ jobs:
|
||||
node-version: "24"
|
||||
cache: "npm"
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Generate current platform binary package
|
||||
run: npm run generate-binary-packages
|
||||
|
||||
- name: Install generated binary package
|
||||
shell: bash
|
||||
run: |
|
||||
package_dir=$(find dist-platforms -mindepth 1 -maxdepth 1 -type d ! -name .downloads | head -n 1)
|
||||
npm install --no-save --package-lock=false "$package_dir"
|
||||
|
||||
- name: Generate types
|
||||
run: npm run generate-types
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Remove any pre-existing Hugo binary
|
||||
shell: bash
|
||||
run: rm -rf bin/
|
||||
|
||||
- name: Verify bin directory is empty
|
||||
shell: bash
|
||||
- name: Verify package-resolved Hugo
|
||||
run: |
|
||||
if [ -d "bin" ]; then
|
||||
echo "bin/ directory still exists!"
|
||||
ls -la bin/
|
||||
exit 1
|
||||
fi
|
||||
echo "bin/ directory confirmed empty"
|
||||
node -e "
|
||||
import('./dist/hugo.mjs').then(async (m) => {
|
||||
const bin = await m.default();
|
||||
console.log(bin);
|
||||
const { stdout } = await m.execWithOutput('version');
|
||||
console.log(stdout.trim());
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
"
|
||||
|
||||
- name: Run postinstall to trigger fresh install
|
||||
run: node postinstall.js
|
||||
|
||||
- name: Verify Hugo binary exists (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
if [ ! -f "bin/hugo" ]; then
|
||||
echo "Hugo binary not found!"
|
||||
exit 1
|
||||
fi
|
||||
echo "Hugo binary found at bin/hugo"
|
||||
ls -la bin/
|
||||
|
||||
- name: Verify Hugo binary exists (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (-not (Test-Path "bin/hugo.exe")) {
|
||||
Write-Error "Hugo binary not found!"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Hugo binary found at bin/hugo.exe"
|
||||
Get-ChildItem bin/
|
||||
|
||||
- name: Verify Hugo version matches package version
|
||||
shell: bash
|
||||
run: |
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
HUGO_VERSION=$(./bin/hugo.exe version)
|
||||
else
|
||||
HUGO_VERSION=$(./bin/hugo version)
|
||||
fi
|
||||
echo "Package version: $PKG_VERSION"
|
||||
echo "Hugo version: $HUGO_VERSION"
|
||||
if [[ "$HUGO_VERSION" != *"v$PKG_VERSION"* ]]; then
|
||||
echo "Version mismatch!"
|
||||
exit 1
|
||||
fi
|
||||
echo "Version verified successfully"
|
||||
|
||||
- name: Verify Extended version (where supported)
|
||||
shell: bash
|
||||
run: |
|
||||
if [[ "$RUNNER_OS" == "Windows" ]]; then
|
||||
HUGO_VERSION=$(./bin/hugo.exe version)
|
||||
else
|
||||
HUGO_VERSION=$(./bin/hugo version)
|
||||
fi
|
||||
# Extended is supported on all GitHub Actions runners (x64)
|
||||
if [[ "$HUGO_VERSION" != *"+extended"* ]]; then
|
||||
echo "Expected Extended version but got: $HUGO_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
echo "Extended version verified"
|
||||
|
||||
# macOS-specific tests (pkg extraction via pkgutil, no sudo required)
|
||||
macos-quirks:
|
||||
name: macOS quirks
|
||||
needs: unit
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- 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: Remove any pre-existing Hugo binary
|
||||
run: rm -rf bin/
|
||||
|
||||
- name: Run postinstall
|
||||
run: node postinstall.js
|
||||
|
||||
- name: Verify binary is regular file (not symlink)
|
||||
run: |
|
||||
if [ -L "bin/hugo" ]; then
|
||||
echo "bin/hugo should not be a symlink (we use pkgutil extraction now)!"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "bin/hugo" ]; then
|
||||
echo "bin/hugo is not a regular file!"
|
||||
exit 1
|
||||
fi
|
||||
echo "Binary is a regular file (extracted from .pkg via pkgutil)"
|
||||
|
||||
- name: Verify executable permissions
|
||||
run: |
|
||||
if [ ! -x "bin/hugo" ]; then
|
||||
echo "bin/hugo is not executable!"
|
||||
exit 1
|
||||
fi
|
||||
echo "Permissions verified"
|
||||
|
||||
- name: Verify binary runs
|
||||
run: ./bin/hugo version
|
||||
|
||||
# Linux- tests (tar.gz extraction, permissions)
|
||||
linux-quirks:
|
||||
name: Linux quirks
|
||||
pack:
|
||||
name: Pack Dry Run
|
||||
needs: unit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -290,180 +179,17 @@ jobs:
|
||||
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: Generate current platform binary package
|
||||
run: npm run generate-binary-packages
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Root package dry run
|
||||
run: npm pack --dry-run
|
||||
|
||||
- name: Remove any pre-existing Hugo binary
|
||||
run: rm -rf bin/
|
||||
|
||||
- name: Run postinstall
|
||||
run: node postinstall.js
|
||||
|
||||
- name: Verify binary is regular file (not symlink)
|
||||
run: |
|
||||
if [ -L "bin/hugo" ]; then
|
||||
echo "bin/hugo should not be a symlink on Linux!"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "bin/hugo" ]; then
|
||||
echo "bin/hugo is not a regular file!"
|
||||
exit 1
|
||||
fi
|
||||
echo "Binary is a regular file"
|
||||
|
||||
- name: Verify executable permissions
|
||||
run: |
|
||||
if [ ! -x "bin/hugo" ]; then
|
||||
echo "bin/hugo is not executable!"
|
||||
exit 1
|
||||
fi
|
||||
PERMS=$(stat -c "%a" bin/hugo)
|
||||
echo "Permissions: $PERMS"
|
||||
# Should have at least 755 (owner rwx, group rx, others rx)
|
||||
if [ "$PERMS" -lt "755" ]; then
|
||||
echo "Insufficient permissions!"
|
||||
exit 1
|
||||
fi
|
||||
echo "Permissions verified"
|
||||
|
||||
- name: Verify binary runs
|
||||
run: ./bin/hugo version
|
||||
|
||||
# Windows- tests (zip extraction)
|
||||
windows-quirks:
|
||||
name: Windows quirks
|
||||
needs: unit
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- 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: Remove any pre-existing Hugo binary
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (Test-Path "bin") {
|
||||
Remove-Item -Recurse -Force "bin"
|
||||
}
|
||||
|
||||
- name: Run postinstall
|
||||
run: node postinstall.js
|
||||
|
||||
- name: Verify binary exists with correct extension
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (-not (Test-Path "bin/hugo.exe")) {
|
||||
Write-Error "bin/hugo.exe not found!"
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Binary found: bin/hugo.exe"
|
||||
Get-ChildItem bin/
|
||||
|
||||
- name: Verify binary runs
|
||||
run: ./bin/hugo.exe version
|
||||
|
||||
# Re-installation test - simulates the "Hugo disappeared" recovery path
|
||||
reinstall:
|
||||
name: Re-installation Recovery (${{ matrix.os }})
|
||||
needs: unit
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- 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: Initial install
|
||||
run: node postinstall.js
|
||||
|
||||
- name: Verify initial install (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: ./bin/hugo version
|
||||
|
||||
- name: Verify initial install (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
run: ./bin/hugo.exe version
|
||||
|
||||
- name: Remove Hugo binary to simulate disappearance
|
||||
- name: Binary package dry run
|
||||
shell: bash
|
||||
run: rm -rf bin/
|
||||
|
||||
- name: Run Node.js script that triggers auto-reinstall
|
||||
run: |
|
||||
node -e "
|
||||
import('./dist/hugo.mjs').then(async (m) => {
|
||||
const path = await m.default();
|
||||
console.log('Hugo reinstalled at:', path);
|
||||
const { execWithOutput } = m;
|
||||
const { stdout } = await execWithOutput('version');
|
||||
console.log('Version:', stdout.trim());
|
||||
}).catch(e => {
|
||||
console.error('Failed:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
"
|
||||
|
||||
- name: Verify Hugo was reinstalled (Unix)
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
if [ ! -f "bin/hugo" ]; then
|
||||
echo "Hugo was not reinstalled!"
|
||||
exit 1
|
||||
fi
|
||||
./bin/hugo version
|
||||
|
||||
- name: Verify Hugo was reinstalled (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (-not (Test-Path "bin/hugo.exe")) {
|
||||
Write-Error "Hugo was not reinstalled!"
|
||||
exit 1
|
||||
}
|
||||
./bin/hugo.exe version
|
||||
package_dir=$(find dist-platforms -mindepth 1 -maxdepth 1 -type d ! -name .downloads | head -n 1)
|
||||
npm pack --dry-run "$package_dir"
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ node_modules/
|
||||
npm-debug.log*
|
||||
*.tsbuildinfo
|
||||
dist/
|
||||
bin/
|
||||
dist-platforms/
|
||||
src/generated/
|
||||
mysite/
|
||||
coverage/
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"sortImports": {},
|
||||
"sortPackageJson": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.json", "**/*.jsonc"],
|
||||
"options": {
|
||||
"trailingComma": "none"
|
||||
}
|
||||
}
|
||||
],
|
||||
"ignorePatterns": ["node_modules", "dist", "dist-platforms", "src/generated"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["oxc", "eslint", "typescript", "import", "unicorn"],
|
||||
"env": {
|
||||
"builtin": true
|
||||
},
|
||||
"categories": {
|
||||
"correctness": "error",
|
||||
"suspicious": "warn"
|
||||
},
|
||||
"rules": {
|
||||
"import/no-named-as-default": "off",
|
||||
"unicorn/consistent-function-scoping": "off",
|
||||
"unicorn/no-array-sort": "off"
|
||||
},
|
||||
"ignorePatterns": ["node_modules", "dist", "dist-platforms", "src/generated"]
|
||||
}
|
||||
@@ -17,28 +17,29 @@ Notes for LLM coding agents working on `hugo-extended`.
|
||||
- **Public API**: `src/hugo.ts`
|
||||
- `default export`: callable function that returns the Hugo binary path **and** has builder methods attached.
|
||||
- Named exports:
|
||||
- `getHugoBinary` (binary resolution + auto-install if missing)
|
||||
- `getHugoBinary` (binary resolution via `HUGO_BIN_PATH` or platform package)
|
||||
- `exec` / `execWithOutput` (spawn Hugo with argv built from options)
|
||||
- `hugo` (builder object)
|
||||
|
||||
- **CLI entry**: `src/cli.ts`
|
||||
- Resolves the binary path via the default export and forwards `process.argv.slice(2)` to Hugo.
|
||||
|
||||
- **Binary installation**: `src/lib/install.ts`
|
||||
- Downloads Hugo release assets and verifies SHA-256 checksums.
|
||||
- **macOS v0.153.0+**: uses `pkgutil --expand-full` to extract the binary from the `.pkg` file (no sudo required).
|
||||
- **macOS pre-v0.153.0**: extracts `.tar.gz` archive into `bin/`.
|
||||
- **non-macOS**: extracts archive into `bin/` and `chmod +x`.
|
||||
- **Platform binary packages**: `src/lib/platform.ts`
|
||||
- Maps supported platform/architecture pairs to exact optional npm packages under `@jakejarvis`.
|
||||
- Extended packages use `hugo-extended-*` names only where upstream Hugo ships Extended.
|
||||
- Windows ARM64 uses the vanilla `@jakejarvis/hugo-windows-arm64` package.
|
||||
|
||||
- **Binary package generation**: `scripts/generate-binary-packages.ts`
|
||||
- Downloads Hugo release assets and verifies SHA-256 checksums during release packaging.
|
||||
- **macOS v0.153.0+**: uses `pkgutil --expand-full` to extract the binary from the `.pkg` file, so the macOS package must be generated on macOS.
|
||||
- `.tar.gz` and `.zip` assets can be generated on Linux/macOS.
|
||||
- Emits publishable package directories in `dist-platforms/`.
|
||||
|
||||
- **Environment variables**: `src/lib/env.ts`
|
||||
- Centralized handling of all `HUGO_*` environment variables.
|
||||
- Exports `getEnvConfig()` for reading parsed config, `logger` for quiet-aware logging.
|
||||
- Centralized handling of `HUGO_BIN_PATH`.
|
||||
- Exports `getEnvConfig()` for reading parsed config.
|
||||
- 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.
|
||||
|
||||
- **Argv builder**: `src/lib/args.ts`
|
||||
- Builds argv using `src/generated/flags.json` to understand flag kinds and canonical long names.
|
||||
- Important: **when a flag exists in the generated spec, its long name is used as-is** (e.g. `--baseURL`, `--buildDrafts`).
|
||||
@@ -83,8 +84,8 @@ npm run test:coverage # coverage via v8
|
||||
- Fast, pure TS/JS (no Hugo execution).
|
||||
- Example: `tests/unit/args.test.ts` covers argv building behavior driven by `flags.json`.
|
||||
- Example: `tests/unit/types.test.ts` uses `expectTypeOf` to validate type surfaces.
|
||||
- Example: `tests/unit/utils.test.ts` covers platform detection, release filename resolution.
|
||||
- Example: `tests/unit/install.test.ts` covers checksum parsing, archive type detection.
|
||||
- Example: `tests/unit/utils.test.ts` covers platform package detection and binary path resolution.
|
||||
- Example: `tests/unit/binary-packages.test.ts` covers generated package manifests, checksum parsing, and archive type detection.
|
||||
|
||||
- `tests/integration/*`
|
||||
- Executes real Hugo commands and does real filesystem work in temp dirs.
|
||||
@@ -92,8 +93,8 @@ npm run test:coverage # coverage via v8
|
||||
- Prefer passing Hugo's global `--source` via `{ source: sitePath }`.
|
||||
|
||||
- `tests/e2e/*`
|
||||
- End-to-end tests for the full installation pipeline.
|
||||
- Verifies binary installation, permissions, symlinks (macOS), and version matching.
|
||||
- End-to-end tests for the resolved Hugo binary.
|
||||
- Verifies binary presence, permissions, and version matching.
|
||||
- Platform-specific tests use `it.skipIf()` to skip on unsupported platforms.
|
||||
|
||||
### Integration test expectations to keep in mind
|
||||
@@ -110,35 +111,25 @@ npm run test:coverage # coverage via v8
|
||||
- Re-run `npm run generate-types` if the change depends on spec shape.
|
||||
- Prefer making tests match **the committed generated spec**, not an assumed kebab-case transform.
|
||||
|
||||
- If you touch installation (`src/lib/install.ts` / `postinstall.js`):
|
||||
- macOS install path uses `sudo installer` and will behave differently in CI/sandboxed environments.
|
||||
- Tests are intentionally focused on the wrapper behavior, not on end-to-end installer reliability.
|
||||
- If you touch binary package generation (`scripts/generate-binary-packages.ts`):
|
||||
- macOS `.pkg` extraction requires macOS `pkgutil`.
|
||||
- Keep generated package manifests script-free.
|
||||
- Use exact package versions that match the root package/Hugo version.
|
||||
|
||||
- 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).
|
||||
- `HUGO_BIN_PATH` is the only supported runtime override.
|
||||
- Use `getEnvConfig()` to read config.
|
||||
|
||||
## 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 |
|
||||
| Variable | Type | Description |
|
||||
| --------------- | ------ | ------------------------------ |
|
||||
| `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, extracted locally using `pkgutil --expand-full` (no sudo required).
|
||||
- **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.
|
||||
- **macOS v0.153.0+**: Hugo ships as a `.pkg`; release packaging extracts it using `pkgutil --expand-full`.
|
||||
- Runtime code does not download or extract Hugo.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 **Zero configuration** — Hugo binary is automatically downloaded on install
|
||||
- 🚀 **Zero configuration** — Hugo binary is provided by a platform-specific optional package
|
||||
- 📦 **Version-locked** — Package version matches Hugo version (e.g., `hugo-extended@0.140.0` = Hugo v0.140.0)
|
||||
- 🔒 **Type-safe API** — Full TypeScript support with autocomplete for all Hugo commands and flags
|
||||
- ⚡ **Multiple APIs** — Use CLI, function-based, or builder-style APIs
|
||||
@@ -40,7 +40,7 @@ These integrate seamlessly with Hugo's [built-in PostCSS pipes](https://gohugo.i
|
||||
|
||||
The simplest way — just run `hugo` commands directly:
|
||||
|
||||
```jsonc
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
@@ -184,77 +184,63 @@ await hugo.server({ port: 3000 });
|
||||
|
||||
All Hugo commands are fully typed with autocomplete:
|
||||
|
||||
| Command | Builder Method | Description |
|
||||
|---------|---------------|-------------|
|
||||
| `build` | `hugo.build()` | Build your site |
|
||||
| `server` | `hugo.server()` | Start dev server |
|
||||
| `new` | `hugo.new()` | Create new content |
|
||||
| `mod get` | `hugo.mod.get()` | Download modules |
|
||||
| `mod tidy` | `hugo.mod.tidy()` | Clean go.mod/go.sum |
|
||||
| `mod clean` | `hugo.mod.clean()` | Clean module cache |
|
||||
| `mod vendor` | `hugo.mod.vendor()` | Vendor dependencies |
|
||||
| `list all` | `hugo.list.all()` | List all content |
|
||||
| `list drafts` | `hugo.list.drafts()` | List draft content |
|
||||
| `config` | `hugo.config()` | Print configuration |
|
||||
| `version` | `hugo.version()` | Print version |
|
||||
| `env` | `hugo.env()` | Print environment |
|
||||
| ... | ... | [All Hugo commands supported](https://gohugo.io/commands/) |
|
||||
| Command | Builder Method | Description |
|
||||
| ------------- | -------------------- | ---------------------------------------------------------- |
|
||||
| `build` | `hugo.build()` | Build your site |
|
||||
| `server` | `hugo.server()` | Start dev server |
|
||||
| `new` | `hugo.new()` | Create new content |
|
||||
| `mod get` | `hugo.mod.get()` | Download modules |
|
||||
| `mod tidy` | `hugo.mod.tidy()` | Clean go.mod/go.sum |
|
||||
| `mod clean` | `hugo.mod.clean()` | Clean module cache |
|
||||
| `mod vendor` | `hugo.mod.vendor()` | Vendor dependencies |
|
||||
| `list all` | `hugo.list.all()` | List all content |
|
||||
| `list drafts` | `hugo.list.drafts()` | List draft content |
|
||||
| `config` | `hugo.config()` | Print configuration |
|
||||
| `version` | `hugo.version()` | Print version |
|
||||
| `env` | `hugo.env()` | Print environment |
|
||||
| ... | ... | [All Hugo commands supported](https://gohugo.io/commands/) |
|
||||
|
||||
## Platform Support
|
||||
|
||||
Hugo Extended is automatically used on supported platforms:
|
||||
|
||||
| Platform | Architecture | Hugo Extended |
|
||||
|----------|-------------|---------------|
|
||||
| macOS | x64, ARM64 | ✅ |
|
||||
| Linux | x64, ARM64 | ✅ |
|
||||
| Windows | x64 | ✅ |
|
||||
| Windows | ARM64 | ❌ (vanilla Hugo) |
|
||||
| FreeBSD | x64 | ❌ (vanilla Hugo) |
|
||||
| Platform | Architecture | Hugo Extended |
|
||||
| -------- | ------------ | ----------------- |
|
||||
| macOS | x64, ARM64 | ✅ |
|
||||
| Linux | x64, ARM64 | ✅ |
|
||||
| Windows | x64 | ✅ |
|
||||
| Windows | ARM64 | ❌ (vanilla Hugo) |
|
||||
|
||||
## Environment Variables
|
||||
|
||||
Customize installation and runtime behavior with these environment variables:
|
||||
Customize runtime binary resolution with this environment variable:
|
||||
|
||||
| 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. |
|
||||
| Variable | Description |
|
||||
| --------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| `HUGO_BIN_PATH` | Use a pre-existing Hugo binary instead of the platform package binary. Example: `HUGO_BIN_PATH=/usr/local/bin/hugo` |
|
||||
|
||||
### 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
|
||||
# Use a system-installed Hugo binary
|
||||
HUGO_BIN_PATH=/usr/local/bin/hugo npm run build
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Hugo binary not found
|
||||
|
||||
If Hugo seems to disappear (rare edge case), it will be automatically reinstalled on next use. You can also manually trigger reinstallation:
|
||||
`hugo-extended` depends on platform-specific optional packages such as `@jakejarvis/hugo-extended-linux-amd64`. If you install with optional dependencies omitted, the wrapper cannot find its bundled Hugo binary.
|
||||
|
||||
```sh
|
||||
npm rebuild hugo-extended
|
||||
npm install
|
||||
```
|
||||
|
||||
If your environment intentionally omits optional dependencies, set `HUGO_BIN_PATH` to a compatible Hugo binary.
|
||||
|
||||
### macOS installation
|
||||
|
||||
As of [v0.153.0](https://github.com/gohugoio/hugo/releases/tag/v0.153.0), Hugo is distributed as a `.pkg` installer for macOS. This package extracts the binary locally using `pkgutil --expand-full`, so **no sudo or global installation is required**. The Hugo binary stays in `node_modules` just like on other platforms.
|
||||
As of [v0.153.0](https://github.com/gohugoio/hugo/releases/tag/v0.153.0), Hugo is distributed as a `.pkg` installer for macOS. The macOS binary package is built from that `.pkg` during release packaging, so **no sudo, global installation, or install-time script is required** for consumers.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": true,
|
||||
"includes": ["**", "!src/generated", "!!**/dist"]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"assist": {
|
||||
"enabled": true,
|
||||
"actions": {
|
||||
"source": {
|
||||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1439
-449
File diff suppressed because it is too large
Load Diff
+47
-51
@@ -2,30 +2,36 @@
|
||||
"name": "hugo-extended",
|
||||
"version": "0.163.3",
|
||||
"description": "✏️ Plug-and-play binary wrapper for Hugo Extended, the awesomest static-site generator.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/jakejarvis/hugo-extended.git"
|
||||
},
|
||||
"keywords": [
|
||||
"cli",
|
||||
"gohugoio",
|
||||
"hugo",
|
||||
"hugo-extended"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/jakejarvis/hugo-extended/issues"
|
||||
},
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
"name": "Jake Jarvis",
|
||||
"email": "jake@jarv.is",
|
||||
"url": "https://github.com/jakejarvis"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/jakejarvis/hugo-extended.git"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "./dist/hugo.mjs",
|
||||
"module": "./dist/hugo.mjs",
|
||||
"types": "./dist/hugo.d.mts",
|
||||
"bin": {
|
||||
"hugo": "dist/cli.mjs",
|
||||
"hugo-extended": "dist/cli.mjs"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "./dist/hugo.mjs",
|
||||
"module": "./dist/hugo.mjs",
|
||||
"types": "./dist/hugo.d.mts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/hugo.d.mts",
|
||||
@@ -37,61 +43,51 @@
|
||||
"default": "./dist/generated/types.mjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"postinstall.js"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"dev": "tsdown --watch",
|
||||
"generate-types": "bun scripts/generate-types.ts",
|
||||
"lint": "biome check",
|
||||
"lint:fix": "biome check --write",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"generate-types": "npx jiti scripts/generate-types.ts",
|
||||
"generate-binary-packages": "npx jiti scripts/generate-binary-packages.ts",
|
||||
"sync-binary-package-versions": "npx jiti scripts/sync-binary-package-versions.ts",
|
||||
"lint": "oxlint",
|
||||
"lint:fix": "oxlint --fix",
|
||||
"fmt": "oxfmt",
|
||||
"fmt:check": "oxfmt --check",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:unit": "vitest run tests/unit",
|
||||
"test:integration": "vitest run tests/integration",
|
||||
"test:e2e": "vitest run tests/e2e",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"postinstall": "node postinstall.js",
|
||||
"pack:dry-run": "npm pack --dry-run && npm run generate-binary-packages -- --pack-dry-run",
|
||||
"prepublishOnly": "npm run generate-types && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.17",
|
||||
"tar": "^7.5.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.15",
|
||||
"@types/adm-zip": "^0.5.8",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/node": "^26.0.0",
|
||||
"@types/tar": "^6.1.13",
|
||||
"@vitest/coverage-v8": "^4.1.7",
|
||||
"tinyexec": "^1.2.2",
|
||||
"tsdown": "^0.22.0",
|
||||
"@vitest/coverage-v8": "^4.1.9",
|
||||
"adm-zip": "^0.5.17",
|
||||
"oxfmt": "^0.55.0",
|
||||
"oxlint": "^1.70.0",
|
||||
"tar": "^7.5.16",
|
||||
"tinyexec": "^1.2.4",
|
||||
"tsdown": "^0.22.3",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^4.1.7"
|
||||
"vitest": "^4.1.9"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@jakejarvis/hugo-extended-darwin-universal": "0.163.3",
|
||||
"@jakejarvis/hugo-extended-linux-amd64": "0.163.3",
|
||||
"@jakejarvis/hugo-extended-linux-arm64": "0.163.3",
|
||||
"@jakejarvis/hugo-extended-windows-amd64": "0.163.3",
|
||||
"@jakejarvis/hugo-windows-arm64": "0.163.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
},
|
||||
"keywords": [
|
||||
"hugo",
|
||||
"hugo extended",
|
||||
"gohugoio",
|
||||
"cli",
|
||||
"front-end",
|
||||
"bin",
|
||||
"binary",
|
||||
"wrapper",
|
||||
"static site generator",
|
||||
"static-site",
|
||||
"ssg",
|
||||
"static",
|
||||
"markdown",
|
||||
"blog",
|
||||
"frontmatter",
|
||||
"go",
|
||||
"golang"
|
||||
]
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
/**
|
||||
* Postinstall wrapper script that properly handles errors during Hugo binary installation.
|
||||
* This script imports and executes the install function, logging any errors with full stack traces
|
||||
* and exiting with a non-zero code on failure.
|
||||
*
|
||||
* 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(
|
||||
"Skipping Hugo installation (dist not found - likely in CI or development environment)",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
const m = await import("./dist/lib/install.mjs");
|
||||
await m.default();
|
||||
} catch (error) {
|
||||
console.error("Hugo installation failed:");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,327 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
import AdmZip from "adm-zip";
|
||||
import * as tar from "tar";
|
||||
|
||||
import {
|
||||
getPlatformPackage,
|
||||
getPlatformPackageByName,
|
||||
HUGO_PLATFORM_PACKAGES,
|
||||
type HugoPlatformPackage,
|
||||
} from "../src/lib/platform";
|
||||
import { getChecksumFilename, getReleaseUrl } from "../src/lib/utils";
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT_DIR = path.join(currentDir, "..");
|
||||
export const BINARY_PACKAGES_DIR = path.join(ROOT_DIR, "dist-platforms");
|
||||
|
||||
export type ArchiveType = "zip" | "tar.gz" | "pkg" | null;
|
||||
|
||||
interface GenerateOptions {
|
||||
outputDir?: string;
|
||||
version?: string;
|
||||
packDryRun?: boolean;
|
||||
}
|
||||
|
||||
interface PackageJson {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
license: string;
|
||||
repository: {
|
||||
type: "git";
|
||||
url: string;
|
||||
};
|
||||
os: NodeJS.Platform[];
|
||||
cpu: NodeJS.Architecture[];
|
||||
preferUnplugged: true;
|
||||
publishConfig: {
|
||||
access: "public";
|
||||
};
|
||||
files: string[];
|
||||
}
|
||||
|
||||
export function getArchiveType(filename: string): ArchiveType {
|
||||
if (filename.endsWith(".zip")) return "zip";
|
||||
if (filename.endsWith(".tar.gz")) return "tar.gz";
|
||||
if (filename.endsWith(".pkg")) return "pkg";
|
||||
return null;
|
||||
}
|
||||
|
||||
export function parseChecksumFile(content: string): Map<string, string> {
|
||||
const checksums = new Map<string, string>();
|
||||
|
||||
for (const line of content.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
const tokens = trimmed.split(/\s+/);
|
||||
if (tokens.length >= 2) {
|
||||
const hash = tokens[0] as string;
|
||||
const filename = tokens[tokens.length - 1] as string;
|
||||
checksums.set(filename, hash);
|
||||
}
|
||||
}
|
||||
|
||||
return checksums;
|
||||
}
|
||||
|
||||
export function createBinaryPackageJson(pkg: HugoPlatformPackage, version: string): PackageJson {
|
||||
return {
|
||||
name: pkg.packageName,
|
||||
version,
|
||||
description: `${pkg.extended ? "Hugo Extended" : "Hugo"} binary for ${pkg.os.join(", ")}/${pkg.cpu.join(", ")}`,
|
||||
license: "MIT",
|
||||
repository: {
|
||||
type: "git",
|
||||
url: "git+https://github.com/jakejarvis/hugo-extended.git",
|
||||
},
|
||||
os: [...pkg.os],
|
||||
cpu: [...pkg.cpu],
|
||||
preferUnplugged: true,
|
||||
publishConfig: {
|
||||
access: "public",
|
||||
},
|
||||
files: ["bin", "README.md", "LICENSE"],
|
||||
};
|
||||
}
|
||||
|
||||
export function getBinaryPackageDir(
|
||||
pkg: HugoPlatformPackage,
|
||||
outputDir = BINARY_PACKAGES_DIR,
|
||||
): string {
|
||||
return path.join(outputDir, pkg.directoryName);
|
||||
}
|
||||
|
||||
export function getBinaryPackageBinPath(
|
||||
pkg: HugoPlatformPackage,
|
||||
outputDir = BINARY_PACKAGES_DIR,
|
||||
): string {
|
||||
return path.join(getBinaryPackageDir(pkg, outputDir), "bin", pkg.binaryName);
|
||||
}
|
||||
|
||||
function readRootPackageVersion(): string {
|
||||
const packageJsonPath = path.join(ROOT_DIR, "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
return (packageJson as { version: string }).version;
|
||||
}
|
||||
|
||||
async function downloadFile(url: string, dest: string): Promise<void> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download ${url}: ${response.statusText}`);
|
||||
}
|
||||
if (!response.body) {
|
||||
throw new Error(`No response body from ${url}`);
|
||||
}
|
||||
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(dest));
|
||||
}
|
||||
|
||||
function sha256(filePath: string): string {
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(fs.readFileSync(filePath));
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
async function verifyChecksum(filePath: string, version: string, filename: string): Promise<void> {
|
||||
const checksumFilename = getChecksumFilename(version);
|
||||
const checksumUrl = getReleaseUrl(version, checksumFilename);
|
||||
const response = await fetch(checksumUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download checksums: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const checksums = parseChecksumFile(await response.text());
|
||||
const expected = checksums.get(filename);
|
||||
if (!expected) {
|
||||
throw new Error(`Checksum for ${filename} not found in ${checksumFilename}`);
|
||||
}
|
||||
|
||||
const actual = sha256(filePath);
|
||||
if (actual !== expected) {
|
||||
throw new Error(`Checksum mismatch for ${filename}: expected ${expected}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
|
||||
function findFile(startDir: string, filename: string): string | null {
|
||||
const entries = fs.readdirSync(startDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(startDir, entry.name);
|
||||
if (entry.isFile() && entry.name === filename) {
|
||||
return entryPath;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
const found = findFile(entryPath, filename);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function extractBinary(
|
||||
archivePath: string,
|
||||
pkg: HugoPlatformPackage,
|
||||
destDir: string,
|
||||
): Promise<void> {
|
||||
const archiveType = getArchiveType(archivePath);
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hugo-binary-"));
|
||||
|
||||
try {
|
||||
if (archiveType === "pkg") {
|
||||
if (process.platform !== "darwin") {
|
||||
throw new Error(`Extracting ${path.basename(archivePath)} requires macOS pkgutil`);
|
||||
}
|
||||
execFileSync("pkgutil", ["--expand-full", archivePath, path.join(tempDir, "expanded")], {
|
||||
stdio: "pipe",
|
||||
});
|
||||
} else if (archiveType === "zip") {
|
||||
const zip = new AdmZip(archivePath);
|
||||
zip.extractAllTo(tempDir, true);
|
||||
} else if (archiveType === "tar.gz") {
|
||||
await tar.x({
|
||||
file: archivePath,
|
||||
cwd: tempDir,
|
||||
});
|
||||
} else {
|
||||
throw new Error(`Unexpected archive type for ${path.basename(archivePath)}`);
|
||||
}
|
||||
|
||||
const extractedBinary = findFile(tempDir, pkg.binaryName);
|
||||
if (!extractedBinary) {
|
||||
throw new Error(`Could not find ${pkg.binaryName} in ${path.basename(archivePath)}`);
|
||||
}
|
||||
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
const destPath = path.join(destDir, pkg.binaryName);
|
||||
fs.copyFileSync(extractedBinary, destPath);
|
||||
|
||||
if (pkg.binaryName === "hugo") {
|
||||
fs.chmodSync(destPath, 0o755);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function writePackageFiles(pkg: HugoPlatformPackage, version: string, packageDir: string): void {
|
||||
const manifest = createBinaryPackageJson(pkg, version);
|
||||
|
||||
fs.writeFileSync(path.join(packageDir, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
||||
fs.copyFileSync(path.join(ROOT_DIR, "LICENSE"), path.join(packageDir, "LICENSE"));
|
||||
fs.writeFileSync(
|
||||
path.join(packageDir, "README.md"),
|
||||
`# ${pkg.packageName}\n\n` +
|
||||
`Platform binary package for ${pkg.extended ? "Hugo Extended" : "Hugo"} v${version}.\n\n` +
|
||||
"This package is installed as an optional dependency of `hugo-extended` and is not intended to be used directly.\n",
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateBinaryPackage(
|
||||
pkg: HugoPlatformPackage,
|
||||
options: GenerateOptions = {},
|
||||
): Promise<string> {
|
||||
const version = options.version ?? readRootPackageVersion();
|
||||
const outputDir = options.outputDir ?? BINARY_PACKAGES_DIR;
|
||||
const packageDir = getBinaryPackageDir(pkg, outputDir);
|
||||
const releaseFile = pkg.releaseFilename(version);
|
||||
const releaseUrl = getReleaseUrl(version, releaseFile);
|
||||
const downloadDir = path.join(outputDir, ".downloads");
|
||||
const downloadPath = path.join(downloadDir, releaseFile);
|
||||
|
||||
fs.rmSync(packageDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.mkdirSync(downloadDir, { recursive: true });
|
||||
|
||||
console.info(`Downloading ${releaseFile}`);
|
||||
await downloadFile(releaseUrl, downloadPath);
|
||||
await verifyChecksum(downloadPath, version, releaseFile);
|
||||
|
||||
writePackageFiles(pkg, version, packageDir);
|
||||
await extractBinary(downloadPath, pkg, path.join(packageDir, "bin"));
|
||||
|
||||
if (options.packDryRun) {
|
||||
execFileSync("npm", ["pack", "--dry-run"], {
|
||||
cwd: packageDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
|
||||
console.info(`Generated ${pkg.packageName} at ${packageDir}`);
|
||||
return packageDir;
|
||||
}
|
||||
|
||||
function parsePackageArgument(args: string[]): string | null {
|
||||
const index = args.indexOf("--package");
|
||||
if (index === -1) return null;
|
||||
|
||||
const value = args[index + 1];
|
||||
if (!value) {
|
||||
throw new Error("--package requires a package name");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function selectPackages(args: string[]): HugoPlatformPackage[] {
|
||||
const packageName = parsePackageArgument(args);
|
||||
if (packageName) {
|
||||
const pkg = getPlatformPackageByName(packageName);
|
||||
if (!pkg) {
|
||||
throw new Error(`Unknown binary package: ${packageName}`);
|
||||
}
|
||||
return [pkg];
|
||||
}
|
||||
|
||||
if (args.includes("--all")) {
|
||||
return [...HUGO_PLATFORM_PACKAGES];
|
||||
}
|
||||
|
||||
const current = getPlatformPackage();
|
||||
if (!current) {
|
||||
throw new Error(`No binary package is defined for ${process.platform}/${process.arch}`);
|
||||
}
|
||||
return [current];
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
const selectedPackages = selectPackages(args);
|
||||
const packDryRun = args.includes("--pack-dry-run");
|
||||
const setGithubEnv = args.includes("--set-github-env");
|
||||
|
||||
const generatedDirs: string[] = [];
|
||||
for (const pkg of selectedPackages) {
|
||||
generatedDirs.push(await generateBinaryPackage(pkg, { packDryRun }));
|
||||
}
|
||||
|
||||
if (setGithubEnv) {
|
||||
if (generatedDirs.length !== 1) {
|
||||
throw new Error("--set-github-env requires exactly one generated package");
|
||||
}
|
||||
const pkg = selectedPackages[0];
|
||||
if (!pkg) {
|
||||
throw new Error("No generated package selected");
|
||||
}
|
||||
const binPath = getBinaryPackageBinPath(pkg);
|
||||
if (!process.env.GITHUB_ENV) {
|
||||
throw new Error("GITHUB_ENV is not set");
|
||||
}
|
||||
fs.appendFileSync(process.env.GITHUB_ENV, `HUGO_BIN_PATH=${binPath}${os.EOL}`);
|
||||
console.info(`Set HUGO_BIN_PATH=${binPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
+28
-20
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { x } from "tinyexec";
|
||||
|
||||
import hugo from "../src/hugo";
|
||||
@@ -150,10 +151,12 @@ function extractDefault(desc: string): {
|
||||
const re = /\s*\(default(?:\s+is)?\s+([^)]+)\)\s*$/i;
|
||||
const m = re.exec(desc);
|
||||
if (!m) return { cleaned: desc };
|
||||
const defaultRaw = m[1];
|
||||
if (defaultRaw === undefined) return { cleaned: desc };
|
||||
|
||||
return {
|
||||
cleaned: desc.slice(0, m.index).trimEnd(),
|
||||
defaultRaw: m[1].trim(),
|
||||
defaultRaw: defaultRaw.trim(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,8 +173,10 @@ function extractEnum(desc: string): { cleaned: string; enum?: string[] } {
|
||||
const re = /\(([^()]*\|[^()]*)\)/;
|
||||
const m = re.exec(desc);
|
||||
if (!m) return { cleaned: desc };
|
||||
const enumRaw = m[1];
|
||||
if (enumRaw === undefined) return { cleaned: desc };
|
||||
|
||||
const parts = m[1]
|
||||
const parts = enumRaw
|
||||
.split("|")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
@@ -202,7 +207,10 @@ function parseFlagsFromSection(
|
||||
let last: FlagSpec | null = null;
|
||||
|
||||
for (let i = startIdx; i < lines.length; i++) {
|
||||
const raw = lines[i].replace(/\t/g, " ").trimEnd();
|
||||
const line = lines[i];
|
||||
if (line === undefined) continue;
|
||||
|
||||
const raw = line.replace(/\t/g, " ").trimEnd();
|
||||
|
||||
// Hugo ends with a standard "Use ..." hint; treat that as a hard stop.
|
||||
if (raw.startsWith('Use "hugo ')) return { flags: out, endIdx: i };
|
||||
@@ -238,7 +246,7 @@ function parseFlagsFromSection(
|
||||
const en = extractEnum(desc);
|
||||
desc = en.cleaned;
|
||||
|
||||
out.push({
|
||||
const flag: FlagSpec = {
|
||||
long,
|
||||
short: m.groups.short,
|
||||
typeToken,
|
||||
@@ -246,9 +254,10 @@ function parseFlagsFromSection(
|
||||
description: desc,
|
||||
defaultRaw: def.defaultRaw,
|
||||
enum: en.enum,
|
||||
});
|
||||
};
|
||||
|
||||
last = out[out.length - 1];
|
||||
out.push(flag);
|
||||
last = flag;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -279,7 +288,10 @@ function parseAvailableCommands(helpText: string): string[] {
|
||||
|
||||
const out: string[] = [];
|
||||
for (let i = idx + 1; i < lines.length; i++) {
|
||||
const raw = lines[i].trimEnd();
|
||||
const line = lines[i];
|
||||
if (line === undefined) continue;
|
||||
|
||||
const raw = line.trimEnd();
|
||||
if (raw.trim() === "") continue;
|
||||
|
||||
// Stop on the next section header.
|
||||
@@ -342,10 +354,8 @@ function normalizeLong(long: string) {
|
||||
*/
|
||||
function camelizeIfKebab(name: string) {
|
||||
if (!name.includes("-")) return name;
|
||||
const [first, ...rest] = name.split("-");
|
||||
return (
|
||||
first + rest.map((p) => (p ? p[0].toUpperCase() + p.slice(1) : "")).join("")
|
||||
);
|
||||
const [first = "", ...rest] = name.split("-");
|
||||
return first + rest.map((p) => (p ? p.charAt(0).toUpperCase() + p.slice(1) : "")).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,7 +365,7 @@ function camelizeIfKebab(name: string) {
|
||||
* @returns PascalCase string (e.g. `ModGet`).
|
||||
*/
|
||||
function pascal(tokens: string[]) {
|
||||
return tokens.map((t) => (t ? t[0].toUpperCase() + t.slice(1) : "")).join("");
|
||||
return tokens.map((t) => (t ? t.charAt(0).toUpperCase() + t.slice(1) : "")).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -410,7 +420,7 @@ function emitInterfaces(globalFlags: FlagSpec[], commands: CommandSpec[]) {
|
||||
lines.push("");
|
||||
|
||||
lines.push(`export interface HugoGlobalOptions {`);
|
||||
for (const f of globalFlags.sort((a, b) => a.long.localeCompare(b.long))) {
|
||||
for (const f of globalFlags.slice().sort((a, b) => a.long.localeCompare(b.long))) {
|
||||
const prop = camelizeIfKebab(normalizeLong(f.long));
|
||||
const tsType = kindToTs(f.kind, f.enum);
|
||||
const def = f.defaultRaw ? ` (default ${f.defaultRaw})` : "";
|
||||
@@ -420,12 +430,12 @@ function emitInterfaces(globalFlags: FlagSpec[], commands: CommandSpec[]) {
|
||||
lines.push(`}`);
|
||||
lines.push("");
|
||||
|
||||
for (const cmd of commands.sort((a, b) =>
|
||||
a.pathTokens.join(" ").localeCompare(b.pathTokens.join(" ")),
|
||||
)) {
|
||||
for (const cmd of commands
|
||||
.slice()
|
||||
.sort((a, b) => a.pathTokens.join(" ").localeCompare(b.pathTokens.join(" ")))) {
|
||||
const name = `Hugo${pascal(cmd.pathTokens)}Options`;
|
||||
lines.push(`export interface ${name} extends HugoGlobalOptions {`);
|
||||
for (const f of cmd.flags.sort((a, b) => a.long.localeCompare(b.long))) {
|
||||
for (const f of cmd.flags.slice().sort((a, b) => a.long.localeCompare(b.long))) {
|
||||
const prop = camelizeIfKebab(normalizeLong(f.long));
|
||||
const tsType = kindToTs(f.kind, f.enum);
|
||||
const def = f.defaultRaw ? ` (default ${f.defaultRaw})` : "";
|
||||
@@ -437,9 +447,7 @@ function emitInterfaces(globalFlags: FlagSpec[], commands: CommandSpec[]) {
|
||||
}
|
||||
|
||||
const cmdStrings = commands.map((c) => c.pathTokens.join(" "));
|
||||
lines.push(
|
||||
`export type HugoCommand = ${cmdStrings.map((s) => JSON.stringify(s)).join(" | ")};`,
|
||||
);
|
||||
lines.push(`export type HugoCommand = ${cmdStrings.map((s) => JSON.stringify(s)).join(" | ")};`);
|
||||
lines.push("");
|
||||
lines.push(`export type HugoOptionsFor<C extends HugoCommand> =`);
|
||||
for (const cmd of commands) {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { HUGO_PLATFORM_PACKAGES } from "../src/lib/platform";
|
||||
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const packageJsonPath = path.join(currentDir, "..", "package.json");
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
||||
version: string;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
packageJson.optionalDependencies = Object.fromEntries(
|
||||
HUGO_PLATFORM_PACKAGES.map((pkg) => [pkg.packageName, packageJson.version]),
|
||||
);
|
||||
|
||||
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`);
|
||||
+2
-4
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
import hugo from "./hugo";
|
||||
|
||||
// Handle unexpected promise rejections
|
||||
@@ -26,10 +27,7 @@ process.on("unhandledRejection", (reason) => {
|
||||
process.exitCode = code ?? undefined;
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"Failed to initialize Hugo:",
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
console.error("Failed to initialize Hugo:", err instanceof Error ? err.message : err);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
})();
|
||||
|
||||
+45
-71
@@ -1,25 +1,23 @@
|
||||
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, logger } from "./lib/utils";
|
||||
import { getPlatformPackage } from "./lib/platform";
|
||||
import { doesBinExist, getBinPath } from "./lib/utils";
|
||||
|
||||
/**
|
||||
* Gets the path to the Hugo binary, automatically installing it if it's missing.
|
||||
* Gets the path to the Hugo binary.
|
||||
*
|
||||
* This is the main entry point for the hugo-extended package. It checks if Hugo
|
||||
* is already installed and available, and if not, triggers an automatic installation
|
||||
* before returning the binary path.
|
||||
*
|
||||
* This handles the case where Hugo may mysteriously disappear (see issue #81),
|
||||
* ensuring the binary is always available when this function is called.
|
||||
* This is the main entry point for the hugo-extended package. It checks for a
|
||||
* custom HUGO_BIN_PATH first, then resolves the platform-specific optional npm
|
||||
* package that contains the Hugo binary.
|
||||
*
|
||||
* Environment variables that affect behavior:
|
||||
* - HUGO_BIN_PATH: Use a custom binary path (skips auto-install if missing)
|
||||
* - HUGO_BIN_PATH: Use a custom binary path
|
||||
*
|
||||
* @returns A promise that resolves with the absolute path to the Hugo binary
|
||||
* @throws {Error} If installation fails, the platform is unsupported, or custom binary is missing
|
||||
* @throws {Error} If the platform is unsupported or the binary is missing
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
@@ -33,22 +31,27 @@ 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}`);
|
||||
if (!bin || !doesBinExist(bin)) {
|
||||
throw new Error(`Custom Hugo binary not found at HUGO_BIN_PATH: ${envConfig.binPath}`);
|
||||
}
|
||||
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.
|
||||
logger.warn("Hugo is missing, reinstalling now...");
|
||||
await install();
|
||||
const platformPackage = getPlatformPackage();
|
||||
if (!platformPackage) {
|
||||
throw new Error(
|
||||
`Unsupported platform for hugo-extended: ${process.platform}/${process.arch}. ` +
|
||||
"Set HUGO_BIN_PATH to use a custom Hugo binary.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!bin || !doesBinExist(bin)) {
|
||||
throw new Error(
|
||||
`Hugo binary package not found for ${process.platform}/${process.arch}. ` +
|
||||
`Expected optional dependency ${platformPackage.packageName}. ` +
|
||||
"Reinstall without omitting optional dependencies, or set HUGO_BIN_PATH to a custom Hugo binary.",
|
||||
);
|
||||
}
|
||||
|
||||
return bin;
|
||||
@@ -107,11 +110,7 @@ export async function exec<C extends HugoCommand>(
|
||||
opts = positionalArgsOrOptions;
|
||||
}
|
||||
|
||||
const args = buildArgs(
|
||||
command,
|
||||
positionalArgs,
|
||||
opts as Record<string, unknown>,
|
||||
);
|
||||
const args = buildArgs(command, positionalArgs, opts as Record<string, unknown>);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(bin, args, { stdio: "inherit" });
|
||||
@@ -175,11 +174,7 @@ export async function execWithOutput<C extends HugoCommand>(
|
||||
opts = positionalArgsOrOptions;
|
||||
}
|
||||
|
||||
const args = buildArgs(
|
||||
command,
|
||||
positionalArgs,
|
||||
opts as Record<string, unknown>,
|
||||
);
|
||||
const args = buildArgs(command, positionalArgs, opts as Record<string, unknown>);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const stdoutChunks: Buffer[] = [];
|
||||
@@ -207,9 +202,7 @@ export async function execWithOutput<C extends HugoCommand>(
|
||||
resolve({ stdout, stderr });
|
||||
} else {
|
||||
reject(
|
||||
new Error(
|
||||
`Hugo command failed with exit code ${code}${stderr ? `\n${stderr}` : ""}`,
|
||||
),
|
||||
new Error(`Hugo command failed with exit code ${code}${stderr ? `\n${stderr}` : ""}`),
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -247,14 +240,11 @@ export const hugo = {
|
||||
|
||||
/** Generate shell completion scripts */
|
||||
completion: {
|
||||
bash: (options?: HugoOptionsFor<"completion bash">) =>
|
||||
exec("completion bash", options),
|
||||
fish: (options?: HugoOptionsFor<"completion fish">) =>
|
||||
exec("completion fish", options),
|
||||
bash: (options?: HugoOptionsFor<"completion bash">) => exec("completion bash", options),
|
||||
fish: (options?: HugoOptionsFor<"completion fish">) => exec("completion fish", options),
|
||||
powershell: (options?: HugoOptionsFor<"completion powershell">) =>
|
||||
exec("completion powershell", options),
|
||||
zsh: (options?: HugoOptionsFor<"completion zsh">) =>
|
||||
exec("completion zsh", options),
|
||||
zsh: (options?: HugoOptionsFor<"completion zsh">) => exec("completion zsh", options),
|
||||
},
|
||||
|
||||
/** Print Hugo configuration */
|
||||
@@ -262,12 +252,9 @@ export const hugo = {
|
||||
|
||||
/** Convert content to different formats */
|
||||
convert: {
|
||||
toJSON: (options?: HugoOptionsFor<"convert toJSON">) =>
|
||||
exec("convert toJSON", options),
|
||||
toTOML: (options?: HugoOptionsFor<"convert toTOML">) =>
|
||||
exec("convert toTOML", options),
|
||||
toYAML: (options?: HugoOptionsFor<"convert toYAML">) =>
|
||||
exec("convert toYAML", options),
|
||||
toJSON: (options?: HugoOptionsFor<"convert toJSON">) => exec("convert toJSON", options),
|
||||
toTOML: (options?: HugoOptionsFor<"convert toTOML">) => exec("convert toTOML", options),
|
||||
toYAML: (options?: HugoOptionsFor<"convert toYAML">) => exec("convert toYAML", options),
|
||||
},
|
||||
|
||||
/** Print Hugo environment info */
|
||||
@@ -281,48 +268,35 @@ export const hugo = {
|
||||
|
||||
/** Import your site from others */
|
||||
import: {
|
||||
jekyll: (options?: HugoOptionsFor<"import jekyll">) =>
|
||||
exec("import jekyll", options),
|
||||
jekyll: (options?: HugoOptionsFor<"import jekyll">) => exec("import jekyll", options),
|
||||
},
|
||||
|
||||
/** List various types of content */
|
||||
list: {
|
||||
all: (options?: HugoOptionsFor<"list all">) => exec("list all", options),
|
||||
drafts: (options?: HugoOptionsFor<"list drafts">) =>
|
||||
exec("list drafts", options),
|
||||
expired: (options?: HugoOptionsFor<"list expired">) =>
|
||||
exec("list expired", options),
|
||||
future: (options?: HugoOptionsFor<"list future">) =>
|
||||
exec("list future", options),
|
||||
published: (options?: HugoOptionsFor<"list published">) =>
|
||||
exec("list published", options),
|
||||
drafts: (options?: HugoOptionsFor<"list drafts">) => exec("list drafts", options),
|
||||
expired: (options?: HugoOptionsFor<"list expired">) => exec("list expired", options),
|
||||
future: (options?: HugoOptionsFor<"list future">) => exec("list future", options),
|
||||
published: (options?: HugoOptionsFor<"list published">) => exec("list published", options),
|
||||
},
|
||||
|
||||
/** Module operations */
|
||||
mod: {
|
||||
clean: (options?: HugoOptionsFor<"mod clean">) =>
|
||||
exec("mod clean", options),
|
||||
clean: (options?: HugoOptionsFor<"mod clean">) => exec("mod clean", options),
|
||||
get: (options?: HugoOptionsFor<"mod get">) => exec("mod get", options),
|
||||
graph: (options?: HugoOptionsFor<"mod graph">) =>
|
||||
exec("mod graph", options),
|
||||
graph: (options?: HugoOptionsFor<"mod graph">) => exec("mod graph", options),
|
||||
init: (options?: HugoOptionsFor<"mod init">) => exec("mod init", options),
|
||||
npm: {
|
||||
pack: (options?: HugoOptionsFor<"mod npm pack">) =>
|
||||
exec("mod npm pack", options),
|
||||
pack: (options?: HugoOptionsFor<"mod npm pack">) => exec("mod npm pack", options),
|
||||
},
|
||||
tidy: (options?: HugoOptionsFor<"mod tidy">) => exec("mod tidy", options),
|
||||
vendor: (options?: HugoOptionsFor<"mod vendor">) =>
|
||||
exec("mod vendor", options),
|
||||
verify: (options?: HugoOptionsFor<"mod verify">) =>
|
||||
exec("mod verify", options),
|
||||
vendor: (options?: HugoOptionsFor<"mod vendor">) => exec("mod vendor", options),
|
||||
verify: (options?: HugoOptionsFor<"mod verify">) => exec("mod verify", options),
|
||||
},
|
||||
|
||||
/** Create new content */
|
||||
new: Object.assign(
|
||||
(
|
||||
pathOrOptions?: string | HugoOptionsFor<"new">,
|
||||
options?: HugoOptionsFor<"new">,
|
||||
) => {
|
||||
(pathOrOptions?: string | HugoOptionsFor<"new">, options?: HugoOptionsFor<"new">) => {
|
||||
if (typeof pathOrOptions === "string") {
|
||||
return exec("new", [pathOrOptions], options);
|
||||
}
|
||||
|
||||
+3
-5
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* Flag specification loaded from the generated spec.json file.
|
||||
@@ -43,7 +43,7 @@ let cachedSpec: HugoSpec | null = null;
|
||||
function loadSpec(): HugoSpec {
|
||||
if (cachedSpec) return cachedSpec;
|
||||
|
||||
const specPath = path.join(__dirname, "..", "generated", "flags.json");
|
||||
const specPath = path.join(currentDir, "..", "generated", "flags.json");
|
||||
try {
|
||||
const specText = fs.readFileSync(specPath, "utf8");
|
||||
cachedSpec = JSON.parse(specText) as HugoSpec;
|
||||
@@ -202,9 +202,7 @@ export function buildArgs(
|
||||
* @param value - The value to inspect.
|
||||
* @returns The inferred flag kind.
|
||||
*/
|
||||
function inferKind(
|
||||
value: unknown,
|
||||
): "boolean" | "string" | "number" | "string[]" | "number[]" {
|
||||
function inferKind(value: unknown): "boolean" | "string" | "number" | "string[]" | "number[]" {
|
||||
if (typeof value === "boolean") return "boolean";
|
||||
if (typeof value === "number") return "number";
|
||||
if (Array.isArray(value)) {
|
||||
|
||||
+10
-211
@@ -1,40 +1,13 @@
|
||||
/**
|
||||
* 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.
|
||||
* The package intentionally keeps runtime configuration small. Platform
|
||||
* binaries are provided by optional npm packages, and HUGO_BIN_PATH remains as
|
||||
* the escape hatch for users who want to provide their own Hugo executable.
|
||||
*
|
||||
* @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).
|
||||
*/
|
||||
@@ -43,15 +16,6 @@ function parseString(value: string | undefined): string | 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.
|
||||
*/
|
||||
@@ -65,196 +29,36 @@ function getFirstDefined(names: string[]): string | undefined {
|
||||
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,
|
||||
type: "string",
|
||||
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.
|
||||
* 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 ?? [])]),
|
||||
binPath: parseString(
|
||||
getFirstDefined([ENV_VARS.binPath.name, ...(ENV_VARS.binPath.aliases ?? [])]),
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -268,11 +72,6 @@ export const ENV_VAR_DOCS = Object.entries(ENV_VARS).map(([key, config]) => ({
|
||||
name: config.name,
|
||||
aliases: config.aliases ?? [],
|
||||
description: config.description,
|
||||
type:
|
||||
config.defaultValue === undefined
|
||||
? "string"
|
||||
: typeof config.defaultValue === "boolean"
|
||||
? "boolean"
|
||||
: "string",
|
||||
type: config.type,
|
||||
default: config.defaultValue,
|
||||
}));
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
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,
|
||||
getChecksumFilename,
|
||||
getPkgVersion,
|
||||
getReleaseFilename,
|
||||
getReleaseUrl,
|
||||
isExtended,
|
||||
logger,
|
||||
} from "./utils";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
/**
|
||||
* Archive types supported by the installer.
|
||||
*/
|
||||
export type ArchiveType = "zip" | "tar.gz" | "pkg" | null;
|
||||
|
||||
/**
|
||||
* Detects the archive type from a filename based on its extension.
|
||||
*
|
||||
* @param filename - The filename to check
|
||||
* @returns The detected archive type, or null if unknown
|
||||
*/
|
||||
export function getArchiveType(filename: string): ArchiveType {
|
||||
if (filename.endsWith(".zip")) return "zip";
|
||||
if (filename.endsWith(".tar.gz")) return "tar.gz";
|
||||
if (filename.endsWith(".pkg")) return "pkg";
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a checksums file content into a lookup map.
|
||||
*
|
||||
* The checksums file format is: "sha256hash filename" (hash followed by whitespace and filename).
|
||||
* This is the standard format used by Hugo releases.
|
||||
*
|
||||
* @param content - The raw content of the checksums file
|
||||
* @returns A Map of filename to SHA-256 hash
|
||||
*/
|
||||
export function parseChecksumFile(content: string): Map<string, string> {
|
||||
const checksums = new Map<string, string>();
|
||||
|
||||
for (const line of content.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
const tokens = trimmed.split(/\s+/);
|
||||
if (tokens.length >= 2) {
|
||||
const hash = tokens[0] as string;
|
||||
const filename = tokens[tokens.length - 1] as string;
|
||||
checksums.set(filename, hash);
|
||||
}
|
||||
}
|
||||
|
||||
return checksums;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads a file from a URL to a local destination path.
|
||||
*
|
||||
* @param url - The URL to download the file from
|
||||
* @param dest - The local file path where the downloaded file will be saved
|
||||
* @throws {Error} If the download fails or the response is invalid
|
||||
* @returns A promise that resolves when the download is complete
|
||||
*/
|
||||
async function downloadFile(url: string, dest: string): Promise<void> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download ${url}: ${response.statusText}`);
|
||||
}
|
||||
if (!response.body) {
|
||||
throw new Error(`No response body from ${url}`);
|
||||
}
|
||||
await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(dest));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a Hugo binary from a macOS .pkg file without requiring sudo.
|
||||
*
|
||||
* Uses `pkgutil --expand-full` to expand the package, then locates and copies
|
||||
* the Hugo binary from the payload to the destination directory.
|
||||
*
|
||||
* The Hugo .pkg structure after expansion contains:
|
||||
* - A "Payload" directory containing the hugo binary directly
|
||||
* - Or a component package directory with Payload inside
|
||||
*
|
||||
* @param pkgPath - The path to the .pkg file to extract
|
||||
* @param destDir - The directory where the hugo binary should be placed
|
||||
* @throws {Error} If extraction fails, Payload is not found, or hugo binary is missing
|
||||
* @see https://github.com/jmooring/hvm/commit/16eb55ae4965b5d2e414061085490a90fe7ea73e
|
||||
*/
|
||||
export function extractPkg(pkgPath: string, destDir: string): void {
|
||||
// Create a temporary directory for expansion
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hugo-pkg-"));
|
||||
|
||||
try {
|
||||
const expansionDir = path.join(tempDir, "expanded");
|
||||
|
||||
// Use pkgutil to expand the package without installing
|
||||
execSync(`pkgutil --expand-full "${pkgPath}" "${expansionDir}"`, {
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
// Find the hugo binary in the expanded package
|
||||
const hugoPayload = path.join(expansionDir, "Payload", "hugo");
|
||||
|
||||
if (!fs.existsSync(hugoPayload)) {
|
||||
throw new Error(
|
||||
"Could not find hugo binary in expanded .pkg. Expected path: */Payload/hugo",
|
||||
);
|
||||
}
|
||||
|
||||
// Copy the binary to the destination
|
||||
const destPath = path.join(destDir, getBinFilename());
|
||||
fs.copyFileSync(hugoPayload, destPath);
|
||||
fs.chmodSync(destPath, 0o755);
|
||||
} finally {
|
||||
// Clean up the temp directory
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that a downloaded file matches its expected SHA-256 checksum.
|
||||
*
|
||||
* Downloads the checksums file from GitHub, extracts the expected checksum for the
|
||||
* specified filename, computes the actual checksum of the local file, and compares them.
|
||||
*
|
||||
* @param filePath - The local path to the file to verify
|
||||
* @param checksumUrl - The URL to the checksums file (usually checksums.txt from the release)
|
||||
* @param filename - The name of the file to find in the checksums file
|
||||
* @throws {Error} If checksums don't match, the checksums file can't be downloaded, or the filename isn't found
|
||||
* @returns A promise that resolves when verification is successful
|
||||
*/
|
||||
async function verifyChecksum(
|
||||
filePath: string,
|
||||
checksumUrl: string,
|
||||
filename: string,
|
||||
): Promise<void> {
|
||||
const response = await fetch(checksumUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download checksums: ${response.statusText}`);
|
||||
}
|
||||
const checksumContent = await response.text();
|
||||
const checksums = parseChecksumFile(checksumContent);
|
||||
|
||||
const expectedChecksum = checksums.get(filename);
|
||||
if (!expectedChecksum) {
|
||||
throw new Error(`Checksum for ${filename} not found in checksums file.`);
|
||||
}
|
||||
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(fileBuffer);
|
||||
const actualChecksum = hash.digest("hex");
|
||||
|
||||
if (actualChecksum !== expectedChecksum) {
|
||||
throw new Error(
|
||||
`Checksum mismatch! Expected ${expectedChecksum}, got ${actualChecksum}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads, verifies, and installs Hugo (Extended when available) for the current platform.
|
||||
*
|
||||
* 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 (or custom mirror)
|
||||
* - Verifies the integrity of the downloaded file using SHA-256 checksums (unless HUGO_SKIP_CHECKSUM is set)
|
||||
* - Extracts the binary (platform-specific):
|
||||
* - macOS v0.153.0+: Extracts from .pkg using pkgutil (no sudo required)
|
||||
* - macOS pre-v0.153.0: Extracts from .tar.gz archive
|
||||
* - Windows: Extracts from .zip archive
|
||||
* - Linux/BSD: Extracts from .tar.gz archive
|
||||
* - 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);
|
||||
const checksumFile = getChecksumFilename(version);
|
||||
const binFile = getBinFilename();
|
||||
|
||||
if (!releaseFile) {
|
||||
throw new Error(
|
||||
`Are you sure this platform is supported? See: https://github.com/gohugoio/hugo/releases/tag/v${version}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!isExtended(releaseFile)) {
|
||||
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
|
||||
const binDir = path.join(__dirname, "..", "..", "bin");
|
||||
if (!fs.existsSync(binDir)) {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
}
|
||||
|
||||
const releaseUrl = getReleaseUrl(version, releaseFile);
|
||||
const checksumUrl = getReleaseUrl(version, checksumFile);
|
||||
const downloadPath = path.join(binDir, releaseFile);
|
||||
|
||||
logger.info(`☁️ Downloading ${releaseFile}...`);
|
||||
await downloadFile(releaseUrl, downloadPath);
|
||||
|
||||
if (envConfig.skipChecksum) {
|
||||
logger.warn(
|
||||
"Skipping checksum verification (HUGO_SKIP_CHECKSUM is set).",
|
||||
);
|
||||
} else {
|
||||
logger.info("🕵️ Verifying checksum...");
|
||||
await verifyChecksum(downloadPath, checksumUrl, releaseFile);
|
||||
}
|
||||
|
||||
// All other platforms and macOS pre-0.153.0 (tar.gz) use archive extraction
|
||||
logger.info("📦 Extracting...");
|
||||
const archiveType = getArchiveType(releaseFile);
|
||||
|
||||
// macOS .pkg files: extract using pkgutil (no sudo required)
|
||||
if (archiveType === "pkg") {
|
||||
extractPkg(downloadPath, binDir);
|
||||
} else if (archiveType === "zip") {
|
||||
const zip = new AdmZip(downloadPath);
|
||||
zip.extractAllTo(binDir, true);
|
||||
} else if (archiveType === "tar.gz") {
|
||||
await tar.x({
|
||||
file: downloadPath,
|
||||
cwd: binDir,
|
||||
});
|
||||
} else {
|
||||
// Defensive: should not happen since unsupported platforms are caught earlier
|
||||
throw new Error(
|
||||
`Unexpected archive type for ${releaseFile}. Expected .zip, .tar.gz, or .pkg.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Cleanup downloaded package
|
||||
fs.unlinkSync(downloadPath);
|
||||
|
||||
const binPath = path.join(binDir, binFile);
|
||||
if (fs.existsSync(binPath)) {
|
||||
fs.chmodSync(binPath, 0o755);
|
||||
}
|
||||
|
||||
logger.info("🎉 Hugo installed successfully!");
|
||||
|
||||
// Check version and return path
|
||||
logger.info(getBinVersion(binPath));
|
||||
return binPath;
|
||||
} catch (error) {
|
||||
logger.error("Hugo installation failed. :(");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default install;
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Platform-specific npm packages that provide Hugo binaries.
|
||||
*
|
||||
* Package names intentionally include "hugo-extended" only where the upstream
|
||||
* Hugo release actually ships the Extended edition for that target.
|
||||
*/
|
||||
|
||||
export interface HugoPlatformPackage {
|
||||
/** npm package containing the binary */
|
||||
packageName: string;
|
||||
/** Directory name used when generating publishable package folders */
|
||||
directoryName: string;
|
||||
/** npm os field values */
|
||||
os: NodeJS.Platform[];
|
||||
/** npm cpu field values */
|
||||
cpu: NodeJS.Architecture[];
|
||||
/** Binary filename inside the package's bin directory */
|
||||
binaryName: "hugo" | "hugo.exe";
|
||||
/** Whether the upstream asset is Hugo Extended */
|
||||
extended: boolean;
|
||||
/** Hugo release asset to download for this package */
|
||||
releaseFilename: (version: string) => string;
|
||||
}
|
||||
|
||||
export const HUGO_PLATFORM_PACKAGES = [
|
||||
{
|
||||
packageName: "@jakejarvis/hugo-extended-darwin-universal",
|
||||
directoryName: "hugo-extended-darwin-universal",
|
||||
os: ["darwin"],
|
||||
cpu: ["x64", "arm64"],
|
||||
binaryName: "hugo",
|
||||
extended: true,
|
||||
releaseFilename: (version: string) => `hugo_extended_${version}_darwin-universal.pkg`,
|
||||
},
|
||||
{
|
||||
packageName: "@jakejarvis/hugo-extended-linux-amd64",
|
||||
directoryName: "hugo-extended-linux-amd64",
|
||||
os: ["linux"],
|
||||
cpu: ["x64"],
|
||||
binaryName: "hugo",
|
||||
extended: true,
|
||||
releaseFilename: (version: string) => `hugo_extended_${version}_linux-amd64.tar.gz`,
|
||||
},
|
||||
{
|
||||
packageName: "@jakejarvis/hugo-extended-linux-arm64",
|
||||
directoryName: "hugo-extended-linux-arm64",
|
||||
os: ["linux"],
|
||||
cpu: ["arm64"],
|
||||
binaryName: "hugo",
|
||||
extended: true,
|
||||
releaseFilename: (version: string) => `hugo_extended_${version}_linux-arm64.tar.gz`,
|
||||
},
|
||||
{
|
||||
packageName: "@jakejarvis/hugo-extended-windows-amd64",
|
||||
directoryName: "hugo-extended-windows-amd64",
|
||||
os: ["win32"],
|
||||
cpu: ["x64"],
|
||||
binaryName: "hugo.exe",
|
||||
extended: true,
|
||||
releaseFilename: (version: string) => `hugo_extended_${version}_windows-amd64.zip`,
|
||||
},
|
||||
{
|
||||
packageName: "@jakejarvis/hugo-windows-arm64",
|
||||
directoryName: "hugo-windows-arm64",
|
||||
os: ["win32"],
|
||||
cpu: ["arm64"],
|
||||
binaryName: "hugo.exe",
|
||||
extended: false,
|
||||
releaseFilename: (version: string) => `hugo_${version}_windows-arm64.zip`,
|
||||
},
|
||||
] as const satisfies readonly HugoPlatformPackage[];
|
||||
|
||||
export function getPlatformPackage(
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
arch: NodeJS.Architecture = process.arch,
|
||||
): HugoPlatformPackage | null {
|
||||
return (
|
||||
HUGO_PLATFORM_PACKAGES.find(
|
||||
(pkg) =>
|
||||
(pkg.os as readonly NodeJS.Platform[]).includes(platform) &&
|
||||
(pkg.cpu as readonly NodeJS.Architecture[]).includes(arch),
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
export function getPlatformPackageSubpath(pkg: HugoPlatformPackage): `bin/${"hugo" | "hugo.exe"}` {
|
||||
return `bin/${pkg.binaryName}`;
|
||||
}
|
||||
|
||||
export function getPlatformPackageByName(packageName: string): HugoPlatformPackage | null {
|
||||
return HUGO_PLATFORM_PACKAGES.find((pkg) => pkg.packageName === packageName) ?? null;
|
||||
}
|
||||
+64
-159
@@ -1,18 +1,20 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { getEnvConfig } from "./env";
|
||||
import {
|
||||
getPlatformPackage,
|
||||
getPlatformPackageSubpath,
|
||||
type HugoPlatformPackage,
|
||||
} from "./platform";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* The first Hugo version that uses .pkg installers for macOS.
|
||||
* Versions before this use .tar.gz archives.
|
||||
*
|
||||
* @see https://github.com/gohugoio/hugo/issues/14135
|
||||
*/
|
||||
const MACOS_PKG_MIN_VERSION = "0.153.0";
|
||||
export type ResolvePackagePath = (specifier: string) => string;
|
||||
|
||||
/**
|
||||
* Compares two semver version strings.
|
||||
@@ -39,68 +41,32 @@ export function compareVersions(a: string, b: string): -1 | 0 | 1 {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a version uses .pkg installers for macOS.
|
||||
* Hugo v0.153.0+ uses .pkg, earlier versions use .tar.gz.
|
||||
* Gets the Hugo version for this package.
|
||||
*
|
||||
* @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.
|
||||
* Package versions are version-locked to Hugo releases.
|
||||
*
|
||||
* 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")
|
||||
* @throws {Error} If package.json cannot be found
|
||||
* @returns The version string (e.g., "0.163.3")
|
||||
*/
|
||||
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");
|
||||
const packageJsonPath = path.join(currentDir, "..", "..", "package.json");
|
||||
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
return (
|
||||
(packageJson as { hugoVersion?: string; version: string }).hugoVersion ||
|
||||
packageJson.version
|
||||
);
|
||||
return (packageJson as { version: string }).version;
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Could not find or read package.json at ${packageJsonPath}`,
|
||||
);
|
||||
throw new Error(`Could not find or read package.json at ${packageJsonPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the full URL to a Hugo release file.
|
||||
* Generates the full GitHub 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")
|
||||
* @param version - The Hugo version number (e.g., "0.163.3")
|
||||
* @param filename - The release filename
|
||||
* @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}`;
|
||||
}
|
||||
|
||||
@@ -113,28 +79,56 @@ export function getBinFilename(): string {
|
||||
return process.platform === "win32" ? "hugo.exe" : "hugo";
|
||||
}
|
||||
|
||||
export function getPlatformPackageBinaryPath(
|
||||
pkg: HugoPlatformPackage,
|
||||
resolvePackagePath: ResolvePackagePath = require.resolve,
|
||||
): string | null {
|
||||
try {
|
||||
return resolvePackagePath(`${pkg.packageName}/${getPlatformPackageSubpath(pkg)}`);
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
"code" in error &&
|
||||
error.code !== "MODULE_NOT_FOUND" &&
|
||||
error.code !== "ERR_MODULE_NOT_FOUND" &&
|
||||
error.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED"
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the absolute path to the Hugo binary.
|
||||
* Gets the absolute path to the Hugo binary if it can be resolved.
|
||||
*
|
||||
* Resolution order:
|
||||
* 1. HUGO_BIN_PATH environment variable (if set)
|
||||
* 2. Local bin directory (./bin/hugo or ./bin/hugo.exe)
|
||||
* 1. HUGO_BIN_PATH environment variable
|
||||
* 2. The matching @jakejarvis platform binary package
|
||||
*
|
||||
* @returns The absolute path to hugo binary
|
||||
* @returns The absolute path to the Hugo binary, or null when no platform
|
||||
* package is available for the current platform/architecture.
|
||||
*/
|
||||
export function getBinPath(): string {
|
||||
export function getBinPath(): string | null {
|
||||
const envConfig = getEnvConfig();
|
||||
if (envConfig.binPath) {
|
||||
return envConfig.binPath;
|
||||
}
|
||||
return path.join(__dirname, "..", "..", "bin", getBinFilename());
|
||||
|
||||
const pkg = getPlatformPackage();
|
||||
if (!pkg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getPlatformPackageBinaryPath(pkg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the Hugo binary and returns its version string.
|
||||
*
|
||||
* @param bin - The absolute path to the Hugo binary
|
||||
* @returns The version output string (e.g., "hugo v0.88.1-5BC54738+extended darwin/arm64 BuildDate=...")
|
||||
* @returns The version output string
|
||||
* @throws {Error} If the binary cannot be executed
|
||||
*/
|
||||
export function getBinVersion(bin: string): string {
|
||||
@@ -155,13 +149,7 @@ export function doesBinExist(bin: string): boolean {
|
||||
return true;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// something bad happened besides Hugo not existing
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
"code" in error &&
|
||||
error.code !== "ENOENT"
|
||||
) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -173,85 +161,18 @@ export function doesBinExist(bin: string): boolean {
|
||||
/**
|
||||
* Determines the correct Hugo release filename for the current platform and architecture.
|
||||
*
|
||||
* Hugo Extended is available for:
|
||||
* - macOS: x64 and ARM64 (universal binaries as of v0.102.0)
|
||||
* - Linux: x64 and ARM64
|
||||
* - 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"),
|
||||
* or `null` if the platform/architecture combination is not supported
|
||||
* @param version - The Hugo version number (e.g., "0.163.3")
|
||||
* @returns The release filename if supported, or `null` if unsupported
|
||||
*/
|
||||
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 =
|
||||
// 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;
|
||||
return getPlatformPackage()?.releaseFilename(version) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the checksums filename for a given Hugo version.
|
||||
*
|
||||
* @param version - The Hugo version number (e.g., "0.88.1")
|
||||
* @returns The checksums filename (e.g., "hugo_0.88.1_checksums.txt")
|
||||
* @param version - The Hugo version number (e.g., "0.163.3")
|
||||
* @returns The checksums filename (e.g., "hugo_0.163.3_checksums.txt")
|
||||
*/
|
||||
export function getChecksumFilename(version: string): string {
|
||||
return `hugo_${version}_checksums.txt`;
|
||||
@@ -260,38 +181,22 @@ export function getChecksumFilename(version: string): string {
|
||||
/**
|
||||
* Determines if a release filename corresponds to Hugo Extended or vanilla Hugo.
|
||||
*
|
||||
* @param releaseFile - The release filename to check (e.g., "hugo_extended_0.88.1_darwin-universal.pkg")
|
||||
* @param releaseFile - The release filename to check
|
||||
* @returns `true` if the release is Hugo Extended, `false` if it's vanilla Hugo
|
||||
*/
|
||||
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);
|
||||
}
|
||||
console.info(message);
|
||||
},
|
||||
|
||||
/**
|
||||
* Log a warning message (respects HUGO_QUIET).
|
||||
*/
|
||||
warn: (message: string): void => {
|
||||
if (!getEnvConfig().quiet) {
|
||||
console.warn(`⚠ ${message}`);
|
||||
}
|
||||
console.warn(`⚠ ${message}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Log an error message (always shown, even in quiet mode).
|
||||
*/
|
||||
error: (message: string): void => {
|
||||
console.error(`✖ ${message}`);
|
||||
},
|
||||
|
||||
+13
-18
@@ -1,6 +1,8 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, lstatSync, statSync } from "node:fs";
|
||||
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
import hugo, { execWithOutput, getHugoBinary } from "../../src/hugo";
|
||||
import {
|
||||
getBinFilename,
|
||||
@@ -11,16 +13,16 @@ import {
|
||||
} from "../../src/lib/utils";
|
||||
|
||||
/**
|
||||
* End-to-end tests for Hugo installation.
|
||||
* End-to-end tests for Hugo binary resolution.
|
||||
*
|
||||
* These tests verify:
|
||||
* - Binary is installed correctly for the current platform
|
||||
* - Binary is resolved correctly for the current platform
|
||||
* - Binary has correct permissions
|
||||
* - Binary is executable and returns expected version
|
||||
* - Extended version is installed where supported
|
||||
*
|
||||
* Note: These tests use the actual installed Hugo binary and require
|
||||
* npm install/postinstall to have completed successfully.
|
||||
* Note: These tests use the actual resolved Hugo binary. In repo CI,
|
||||
* HUGO_BIN_PATH points at a generated platform binary package.
|
||||
*/
|
||||
describe("Hugo Installation E2E", () => {
|
||||
let binaryPath: string;
|
||||
@@ -48,15 +50,12 @@ describe("Hugo Installation E2E", () => {
|
||||
});
|
||||
|
||||
describe("Binary Permissions (Unix)", () => {
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"should have executable permissions",
|
||||
() => {
|
||||
const stats = statSync(binaryPath);
|
||||
// Check that at least owner has execute permission (0o100)
|
||||
const hasExecute = (stats.mode & 0o100) !== 0;
|
||||
expect(hasExecute).toBe(true);
|
||||
},
|
||||
);
|
||||
it.skipIf(process.platform === "win32")("should have executable permissions", () => {
|
||||
const stats = statSync(binaryPath);
|
||||
// Check that at least owner has execute permission (0o100)
|
||||
const hasExecute = (stats.mode & 0o100) !== 0;
|
||||
expect(hasExecute).toBe(true);
|
||||
});
|
||||
|
||||
it.skipIf(process.platform !== "darwin")(
|
||||
"should be a regular file on macOS (not a symlink)",
|
||||
@@ -137,11 +136,7 @@ describe("Hugo Installation E2E", () => {
|
||||
const { stdout } = await execWithOutput("env");
|
||||
|
||||
const expectedGoarch =
|
||||
process.arch === "x64"
|
||||
? "amd64"
|
||||
: process.arch === "arm64"
|
||||
? "arm64"
|
||||
: process.arch;
|
||||
process.arch === "x64" ? "amd64" : process.arch === "arm64" ? "arm64" : process.arch;
|
||||
|
||||
expect(stdout).toContain(`GOARCH="${expectedGoarch}"`);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
import hugo, { execWithOutput } from "../../src/hugo";
|
||||
|
||||
describe("Hugo Commands Integration", () => {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { access, mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { hugo } from "../../src/hugo";
|
||||
|
||||
describe("New Commands Integration", () => {
|
||||
@@ -23,9 +25,7 @@ describe("New Commands Integration", () => {
|
||||
await hugo.new.project(sitePath);
|
||||
|
||||
// Check that essential directories exist
|
||||
await expect(
|
||||
access(join(sitePath, "hugo.toml")),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(access(join(sitePath, "hugo.toml"))).resolves.toBeUndefined();
|
||||
await expect(access(join(sitePath, "content"))).resolves.toBeUndefined();
|
||||
await expect(access(join(sitePath, "themes"))).resolves.toBeUndefined();
|
||||
});
|
||||
@@ -34,18 +34,14 @@ describe("New Commands Integration", () => {
|
||||
const sitePath = join(tempDir, "test-site-yaml");
|
||||
await hugo.new.project(sitePath, { format: "yaml" });
|
||||
|
||||
await expect(
|
||||
access(join(sitePath, "hugo.yaml")),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(access(join(sitePath, "hugo.yaml"))).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should create a new project with json format", async () => {
|
||||
const sitePath = join(tempDir, "test-site-json");
|
||||
await hugo.new.project(sitePath, { format: "json" });
|
||||
|
||||
await expect(
|
||||
access(join(sitePath, "hugo.json")),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(access(join(sitePath, "hugo.json"))).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should respect force flag", async () => {
|
||||
@@ -59,9 +55,7 @@ describe("New Commands Integration", () => {
|
||||
try {
|
||||
await hugo.new.project(sitePath, { force: true });
|
||||
// If it succeeds, verify the site still exists
|
||||
await expect(
|
||||
access(join(sitePath, "hugo.toml")),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(access(join(sitePath, "hugo.toml"))).resolves.toBeUndefined();
|
||||
} catch (error) {
|
||||
// If it fails, that's the current 0.154.x behavior
|
||||
expect(error).toBeDefined();
|
||||
@@ -79,9 +73,7 @@ describe("New Commands Integration", () => {
|
||||
|
||||
const themePath = join(sitePath, "themes", "test-theme");
|
||||
await expect(access(themePath)).resolves.toBeUndefined();
|
||||
await expect(
|
||||
access(join(themePath, "hugo.toml")),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(access(join(themePath, "hugo.toml"))).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("should create theme with yaml format", async () => {
|
||||
@@ -94,9 +86,7 @@ describe("New Commands Integration", () => {
|
||||
});
|
||||
|
||||
const themePath = join(sitePath, "themes", "test-theme-yaml");
|
||||
await expect(
|
||||
access(join(themePath, "hugo.yaml")),
|
||||
).resolves.toBeUndefined();
|
||||
await expect(access(join(themePath, "hugo.yaml"))).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { buildArgs } from "../../src/lib/args";
|
||||
|
||||
describe("buildArgs", () => {
|
||||
@@ -88,9 +89,7 @@ describe("buildArgs", () => {
|
||||
}, []);
|
||||
|
||||
expect(themeIndices).toHaveLength(3);
|
||||
expect(args[themeIndices[0] + 1]).toBe("a");
|
||||
expect(args[themeIndices[1] + 1]).toBe("b");
|
||||
expect(args[themeIndices[2] + 1]).toBe("c");
|
||||
expect(themeIndices.map((index) => args[index + 1])).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
createBinaryPackageJson,
|
||||
getArchiveType,
|
||||
getBinaryPackageBinPath,
|
||||
getBinaryPackageDir,
|
||||
parseChecksumFile,
|
||||
} from "../../scripts/generate-binary-packages";
|
||||
import { getPlatformPackageByName, HUGO_PLATFORM_PACKAGES } from "../../src/lib/platform";
|
||||
|
||||
describe("binary package generation", () => {
|
||||
describe("package set", () => {
|
||||
it("contains the supported platform packages", () => {
|
||||
expect(HUGO_PLATFORM_PACKAGES.map((pkg) => pkg.packageName)).toEqual([
|
||||
"@jakejarvis/hugo-extended-darwin-universal",
|
||||
"@jakejarvis/hugo-extended-linux-amd64",
|
||||
"@jakejarvis/hugo-extended-linux-arm64",
|
||||
"@jakejarvis/hugo-extended-windows-amd64",
|
||||
"@jakejarvis/hugo-windows-arm64",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createBinaryPackageJson", () => {
|
||||
it("creates a script-free manifest with platform filters", () => {
|
||||
const pkg = getPlatformPackageByName("@jakejarvis/hugo-extended-linux-amd64");
|
||||
if (!pkg) throw new Error("Expected Linux x64 package");
|
||||
|
||||
const manifest = createBinaryPackageJson(pkg, "0.163.3");
|
||||
|
||||
expect(manifest).toMatchObject({
|
||||
name: "@jakejarvis/hugo-extended-linux-amd64",
|
||||
version: "0.163.3",
|
||||
os: ["linux"],
|
||||
cpu: ["x64"],
|
||||
preferUnplugged: true,
|
||||
publishConfig: { access: "public" },
|
||||
files: ["bin", "README.md", "LICENSE"],
|
||||
});
|
||||
expect(manifest).not.toHaveProperty("scripts");
|
||||
expect(manifest).not.toHaveProperty("bin");
|
||||
});
|
||||
|
||||
it("uses vanilla Hugo naming for Windows ARM64", () => {
|
||||
const pkg = getPlatformPackageByName("@jakejarvis/hugo-windows-arm64");
|
||||
if (!pkg) throw new Error("Expected Windows ARM64 package");
|
||||
|
||||
const manifest = createBinaryPackageJson(pkg, "0.163.3");
|
||||
|
||||
expect(manifest.name).toBe("@jakejarvis/hugo-windows-arm64");
|
||||
expect(manifest.description).toContain("Hugo binary");
|
||||
expect(pkg?.releaseFilename("0.163.3")).toBe("hugo_0.163.3_windows-arm64.zip");
|
||||
});
|
||||
});
|
||||
|
||||
describe("paths", () => {
|
||||
it("uses package directory names and binary filenames", () => {
|
||||
const pkg = getPlatformPackageByName("@jakejarvis/hugo-extended-windows-amd64");
|
||||
if (!pkg) throw new Error("Expected Windows x64 package");
|
||||
|
||||
expect(getBinaryPackageDir(pkg, "/tmp/out")).toBe("/tmp/out/hugo-extended-windows-amd64");
|
||||
expect(getBinaryPackageBinPath(pkg, "/tmp/out")).toBe(
|
||||
"/tmp/out/hugo-extended-windows-amd64/bin/hugo.exe",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseChecksumFile", () => {
|
||||
it("parses checksums file format correctly", () => {
|
||||
const checksumContent = `
|
||||
abc123def456 hugo_0.163.3_linux-amd64.tar.gz
|
||||
def789abc012 hugo_extended_0.163.3_linux-amd64.tar.gz
|
||||
ghi345jkl678 hugo_0.163.3_windows-amd64.zip
|
||||
`.trim();
|
||||
|
||||
const checksums = parseChecksumFile(checksumContent);
|
||||
|
||||
expect(checksums.size).toBe(3);
|
||||
expect(checksums.get("hugo_0.163.3_linux-amd64.tar.gz")).toBe("abc123def456");
|
||||
expect(checksums.get("hugo_extended_0.163.3_linux-amd64.tar.gz")).toBe("def789abc012");
|
||||
expect(checksums.get("hugo_0.163.3_windows-amd64.zip")).toBe("ghi345jkl678");
|
||||
});
|
||||
|
||||
it("handles empty content", () => {
|
||||
expect(parseChecksumFile("").size).toBe(0);
|
||||
expect(parseChecksumFile(" \n\n \n").size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getArchiveType", () => {
|
||||
it("detects supported archive types", () => {
|
||||
expect(getArchiveType("hugo_extended_0.163.3_windows-amd64.zip")).toBe("zip");
|
||||
expect(getArchiveType("hugo_extended_0.163.3_linux-amd64.tar.gz")).toBe("tar.gz");
|
||||
expect(getArchiveType("hugo_extended_0.163.3_darwin-universal.pkg")).toBe("pkg");
|
||||
});
|
||||
|
||||
it("returns null for unknown extensions", () => {
|
||||
expect(getArchiveType("hugo_0.163.3_checksums.txt")).toBeNull();
|
||||
expect(getArchiveType("hugo")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
+21
-202
@@ -1,26 +1,12 @@
|
||||
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 envVars = ["HUGO_BIN_PATH"];
|
||||
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];
|
||||
@@ -28,7 +14,6 @@ describe("env", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original values
|
||||
for (const key of envVars) {
|
||||
if (originalEnv[key] === undefined) {
|
||||
delete process.env[key];
|
||||
@@ -39,201 +24,35 @@ describe("env", () => {
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
it("returns undefined when HUGO_BIN_PATH is not set", () => {
|
||||
expect(getEnvConfig()).toEqual({ binPath: undefined });
|
||||
});
|
||||
|
||||
describe("forceStandard", () => {
|
||||
it("should return false when not set", () => {
|
||||
const config = getEnvConfig();
|
||||
expect(config.forceStandard).toBe(false);
|
||||
});
|
||||
it("returns the trimmed value from HUGO_BIN_PATH", () => {
|
||||
process.env.HUGO_BIN_PATH = " /usr/local/bin/hugo ";
|
||||
|
||||
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);
|
||||
});
|
||||
expect(getEnvConfig()).toEqual({ binPath: "/usr/local/bin/hugo" });
|
||||
});
|
||||
|
||||
describe("skipInstall", () => {
|
||||
it("should return false when not set", () => {
|
||||
const config = getEnvConfig();
|
||||
expect(config.skipInstall).toBe(false);
|
||||
});
|
||||
it("ignores empty values", () => {
|
||||
process.env.HUGO_BIN_PATH = " ";
|
||||
|
||||
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);
|
||||
});
|
||||
expect(getEnvConfig()).toEqual({ binPath: undefined });
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
it("documents only HUGO_BIN_PATH", () => {
|
||||
expect(ENV_VAR_DOCS).toEqual([
|
||||
{
|
||||
key: "binPath",
|
||||
name: "HUGO_BIN_PATH",
|
||||
aliases: [],
|
||||
description: "Path to a pre-existing Hugo binary",
|
||||
type: "string",
|
||||
default: undefined,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, assert, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
extractPkg,
|
||||
getArchiveType,
|
||||
parseChecksumFile,
|
||||
} from "../../src/lib/install";
|
||||
import { getReleaseFilename } from "../../src/lib/utils";
|
||||
|
||||
/**
|
||||
* Unit tests for installation logic that can be tested without network calls.
|
||||
* These tests verify:
|
||||
* - Checksum file parsing
|
||||
* - Archive type detection
|
||||
* - SHA-256 computation
|
||||
*/
|
||||
describe("Installation Logic", () => {
|
||||
describe("SHA-256 Computation", () => {
|
||||
it("should correctly compute SHA-256 hash", () => {
|
||||
const testData = "Hello, Hugo!";
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(Buffer.from(testData));
|
||||
const digest = hash.digest("hex");
|
||||
|
||||
// Expected SHA-256 hash for "Hello, Hugo!"
|
||||
// Computed via: echo -n "Hello, Hugo!" | sha256sum
|
||||
// Cross-verified by computing with a second method below
|
||||
const expectedHash =
|
||||
"766a2e18bc3e2f7e217b4566b7988ca3a28e1de8cd70d995219088497a0830e5";
|
||||
|
||||
expect(digest).toBe(expectedHash);
|
||||
expect(digest).toHaveLength(64);
|
||||
|
||||
// Cross-verify by computing with a fresh hash instance
|
||||
const verifyHash = crypto.createHash("sha256");
|
||||
verifyHash.update(testData, "utf8");
|
||||
expect(verifyHash.digest("hex")).toBe(expectedHash);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseChecksumFile", () => {
|
||||
it("should parse checksums file format correctly", () => {
|
||||
const checksumContent = `
|
||||
abc123def456 hugo_0.154.3_linux-amd64.tar.gz
|
||||
def789abc012 hugo_extended_0.154.3_linux-amd64.tar.gz
|
||||
ghi345jkl678 hugo_0.154.3_windows-amd64.zip
|
||||
`.trim();
|
||||
|
||||
const checksums = parseChecksumFile(checksumContent);
|
||||
|
||||
expect(checksums.size).toBe(3);
|
||||
expect(checksums.get("hugo_0.154.3_linux-amd64.tar.gz")).toBe(
|
||||
"abc123def456",
|
||||
);
|
||||
expect(checksums.get("hugo_extended_0.154.3_linux-amd64.tar.gz")).toBe(
|
||||
"def789abc012",
|
||||
);
|
||||
expect(checksums.get("hugo_0.154.3_windows-amd64.zip")).toBe(
|
||||
"ghi345jkl678",
|
||||
);
|
||||
});
|
||||
|
||||
it("should find correct checksum for a given filename", () => {
|
||||
const checksumContent = `
|
||||
abc123def456 hugo_0.154.3_linux-amd64.tar.gz
|
||||
def789abc012 hugo_extended_0.154.3_linux-amd64.tar.gz
|
||||
ghi345jkl678 hugo_0.154.3_windows-amd64.zip
|
||||
`;
|
||||
const checksums = parseChecksumFile(checksumContent);
|
||||
|
||||
expect(checksums.get("hugo_extended_0.154.3_linux-amd64.tar.gz")).toBe(
|
||||
"def789abc012",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return undefined when filename not in checksums", () => {
|
||||
const checksumContent = `
|
||||
abc123def456 hugo_0.154.3_linux-amd64.tar.gz
|
||||
`;
|
||||
const checksums = parseChecksumFile(checksumContent);
|
||||
|
||||
expect(checksums.get("hugo_0.154.3_windows-amd64.zip")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should handle empty content", () => {
|
||||
const checksums = parseChecksumFile("");
|
||||
expect(checksums.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle content with only whitespace lines", () => {
|
||||
const checksums = parseChecksumFile(" \n\n \n");
|
||||
expect(checksums.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should handle real-world Hugo checksums format", () => {
|
||||
// Real format from Hugo releases uses two spaces between hash and filename
|
||||
const realChecksumContent = `
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 hugo_0.154.3_checksums.txt
|
||||
a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890 hugo_extended_0.154.3_darwin-universal.pkg
|
||||
f0e9d8c7b6a5432109876543210fedcba0987654321fedcba0987654321fedc hugo_extended_0.154.3_linux-amd64.tar.gz
|
||||
`;
|
||||
const checksums = parseChecksumFile(realChecksumContent);
|
||||
|
||||
expect(checksums.size).toBe(3);
|
||||
expect(checksums.get("hugo_extended_0.154.3_darwin-universal.pkg")).toBe(
|
||||
"a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getArchiveType", () => {
|
||||
it("should identify zip files", () => {
|
||||
expect(getArchiveType("hugo_extended_0.154.3_windows-amd64.zip")).toBe(
|
||||
"zip",
|
||||
);
|
||||
});
|
||||
|
||||
it("should identify tar.gz files", () => {
|
||||
expect(getArchiveType("hugo_extended_0.154.3_linux-amd64.tar.gz")).toBe(
|
||||
"tar.gz",
|
||||
);
|
||||
});
|
||||
|
||||
it("should identify pkg files", () => {
|
||||
expect(getArchiveType("hugo_extended_0.154.3_darwin-universal.pkg")).toBe(
|
||||
"pkg",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return null for unknown extensions", () => {
|
||||
expect(getArchiveType("hugo_0.154.3_readme.txt")).toBeNull();
|
||||
expect(getArchiveType("hugo.exe")).toBeNull();
|
||||
expect(getArchiveType("checksums.txt")).toBeNull();
|
||||
});
|
||||
|
||||
it("should correctly detect archive type for all platform release filenames", () => {
|
||||
// Windows x64 -> zip
|
||||
expect(getArchiveType("hugo_extended_0.154.3_windows-amd64.zip")).toBe(
|
||||
"zip",
|
||||
);
|
||||
|
||||
// Windows arm64 -> zip
|
||||
expect(getArchiveType("hugo_0.154.3_windows-arm64.zip")).toBe("zip");
|
||||
|
||||
// Linux x64 -> tar.gz
|
||||
expect(getArchiveType("hugo_extended_0.154.3_linux-amd64.tar.gz")).toBe(
|
||||
"tar.gz",
|
||||
);
|
||||
|
||||
// Linux arm64 -> tar.gz
|
||||
expect(getArchiveType("hugo_extended_0.154.3_linux-arm64.tar.gz")).toBe(
|
||||
"tar.gz",
|
||||
);
|
||||
|
||||
// macOS -> pkg
|
||||
expect(getArchiveType("hugo_extended_0.154.3_darwin-universal.pkg")).toBe(
|
||||
"pkg",
|
||||
);
|
||||
|
||||
// FreeBSD -> tar.gz
|
||||
expect(getArchiveType("hugo_0.154.3_freebsd-amd64.tar.gz")).toBe(
|
||||
"tar.gz",
|
||||
);
|
||||
|
||||
// OpenBSD -> tar.gz
|
||||
expect(getArchiveType("hugo_0.154.3_openbsd-amd64.tar.gz")).toBe(
|
||||
"tar.gz",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractPkg", () => {
|
||||
it.skipIf(process.platform !== "darwin")(
|
||||
"should throw error for non-existent .pkg file",
|
||||
() => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hugo-test-"));
|
||||
try {
|
||||
const nonExistentPkg = path.join(tempDir, "nonexistent.pkg");
|
||||
expect(() => extractPkg(nonExistentPkg, tempDir)).toThrow();
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform !== "darwin")(
|
||||
"should throw descriptive error when pkgutil fails on invalid pkg",
|
||||
() => {
|
||||
// Create a temporary directory with a fake .pkg file
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hugo-test-"));
|
||||
try {
|
||||
// Create a fake .pkg file (just an empty file - pkgutil will fail to expand it)
|
||||
const fakePkg = path.join(tempDir, "fake.pkg");
|
||||
fs.writeFileSync(fakePkg, "not a real pkg");
|
||||
|
||||
expect(() => extractPkg(fakePkg, tempDir)).toThrow();
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === "darwin")(
|
||||
"should not be available on non-macOS platforms (pkgutil is macOS-only)",
|
||||
() => {
|
||||
// On non-macOS platforms, pkgutil doesn't exist, so the function will fail
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "hugo-test-"));
|
||||
try {
|
||||
const fakePkg = path.join(tempDir, "fake.pkg");
|
||||
fs.writeFileSync(fakePkg, "");
|
||||
expect(() => extractPkg(fakePkg, tempDir)).toThrow();
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("getReleaseFilename + getArchiveType integration", () => {
|
||||
let originalPlatform: NodeJS.Platform;
|
||||
let originalArch: NodeJS.Architecture;
|
||||
|
||||
beforeEach(() => {
|
||||
originalPlatform = process.platform;
|
||||
originalArch = process.arch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
Object.defineProperty(process, "arch", { value: originalArch });
|
||||
});
|
||||
|
||||
it("should return zip for Windows release filenames", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
|
||||
const filename = getReleaseFilename("0.154.3");
|
||||
assert(filename !== null, "Expected Windows x64 to have a release file");
|
||||
expect(getArchiveType(filename)).toBe("zip");
|
||||
});
|
||||
|
||||
it("should return tar.gz for Linux release filenames", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
|
||||
const filename = getReleaseFilename("0.154.3");
|
||||
assert(filename !== null, "Expected Linux x64 to have a release file");
|
||||
expect(getArchiveType(filename)).toBe("tar.gz");
|
||||
});
|
||||
|
||||
it("should return pkg for macOS release filenames", () => {
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
Object.defineProperty(process, "arch", { value: "arm64" });
|
||||
|
||||
const filename = getReleaseFilename("0.154.3");
|
||||
assert(filename !== null, "Expected macOS arm64 to have a release file");
|
||||
expect(getArchiveType(filename)).toBe("pkg");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expectTypeOf, it } from "vitest";
|
||||
|
||||
import type { HugoCommand, HugoOptionsFor } from "../../src/generated/types";
|
||||
|
||||
describe("Type Safety", () => {
|
||||
|
||||
+106
-341
@@ -1,55 +1,45 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getPlatformPackage } from "../../src/lib/platform";
|
||||
import {
|
||||
compareVersions,
|
||||
getBinFilename,
|
||||
getBinPath,
|
||||
getChecksumFilename,
|
||||
getPkgVersion,
|
||||
getPlatformPackageBinaryPath,
|
||||
getReleaseFilename,
|
||||
getReleaseUrl,
|
||||
isExtended,
|
||||
logger,
|
||||
usesMacOSPkg,
|
||||
} from "../../src/lib/utils";
|
||||
|
||||
describe("utils", () => {
|
||||
describe("compareVersions", () => {
|
||||
it("should return 0 for equal versions", () => {
|
||||
it("returns 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", () => {
|
||||
it("returns -1 when the 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", () => {
|
||||
it("returns 1 when the 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", () => {
|
||||
it("handles 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("getPkgVersion", () => {
|
||||
it("returns the package version", () => {
|
||||
expect(getPkgVersion()).toMatch(/^\d+\.\d+\.\d+/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,24 +54,51 @@ describe("utils", () => {
|
||||
Object.defineProperty(process, "platform", { value: originalPlatform });
|
||||
});
|
||||
|
||||
it("should return hugo.exe on Windows", () => {
|
||||
it("returns hugo.exe on Windows", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
expect(getBinFilename()).toBe("hugo.exe");
|
||||
});
|
||||
|
||||
it("should return hugo on Linux", () => {
|
||||
it("returns hugo on Unix-like platforms", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
expect(getBinFilename()).toBe("hugo");
|
||||
});
|
||||
});
|
||||
|
||||
it("should return hugo on macOS", () => {
|
||||
Object.defineProperty(process, "platform", { value: "darwin" });
|
||||
expect(getBinFilename()).toBe("hugo");
|
||||
describe("platform packages", () => {
|
||||
it("maps macOS x64 and ARM64 to the universal Extended package", () => {
|
||||
expect(getPlatformPackage("darwin", "x64")).toMatchObject({
|
||||
packageName: "@jakejarvis/hugo-extended-darwin-universal",
|
||||
binaryName: "hugo",
|
||||
extended: true,
|
||||
});
|
||||
expect(getPlatformPackage("darwin", "arm64")).toMatchObject({
|
||||
packageName: "@jakejarvis/hugo-extended-darwin-universal",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return hugo on other platforms", () => {
|
||||
Object.defineProperty(process, "platform", { value: "freebsd" });
|
||||
expect(getBinFilename()).toBe("hugo");
|
||||
it("maps Linux x64 and ARM64 to Extended packages", () => {
|
||||
expect(getPlatformPackage("linux", "x64")).toMatchObject({
|
||||
packageName: "@jakejarvis/hugo-extended-linux-amd64",
|
||||
binaryName: "hugo",
|
||||
extended: true,
|
||||
});
|
||||
expect(getPlatformPackage("linux", "arm64")).toMatchObject({
|
||||
packageName: "@jakejarvis/hugo-extended-linux-arm64",
|
||||
});
|
||||
});
|
||||
|
||||
it("maps Windows x64 to Extended and Windows ARM64 to vanilla", () => {
|
||||
expect(getPlatformPackage("win32", "x64")).toMatchObject({
|
||||
packageName: "@jakejarvis/hugo-extended-windows-amd64",
|
||||
binaryName: "hugo.exe",
|
||||
extended: true,
|
||||
});
|
||||
expect(getPlatformPackage("win32", "arm64")).toMatchObject({
|
||||
packageName: "@jakejarvis/hugo-windows-arm64",
|
||||
binaryName: "hugo.exe",
|
||||
extended: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,276 +116,70 @@ describe("utils", () => {
|
||||
Object.defineProperty(process, "arch", { value: originalArch });
|
||||
});
|
||||
|
||||
describe("macOS", () => {
|
||||
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(
|
||||
"hugo_extended_0.154.3_darwin-universal.pkg",
|
||||
);
|
||||
});
|
||||
it("returns the release filename for the current supported platform", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
|
||||
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",
|
||||
);
|
||||
});
|
||||
expect(getReleaseFilename("0.163.3")).toBe("hugo_extended_0.163.3_linux-amd64.tar.gz");
|
||||
});
|
||||
|
||||
describe("Windows", () => {
|
||||
it("should return extended zip for win32 x64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_extended_0.154.3_windows-amd64.zip",
|
||||
);
|
||||
});
|
||||
it("returns null for unsupported platforms", () => {
|
||||
Object.defineProperty(process, "platform", { value: "freebsd" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
|
||||
it("should return vanilla zip for win32 arm64 (no extended support)", () => {
|
||||
Object.defineProperty(process, "platform", { value: "win32" });
|
||||
Object.defineProperty(process, "arch", { value: "arm64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_0.154.3_windows-arm64.zip",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Linux", () => {
|
||||
it("should return extended tar.gz for linux x64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_extended_0.154.3_linux-amd64.tar.gz",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return extended tar.gz for linux arm64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
Object.defineProperty(process, "arch", { value: "arm64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_extended_0.154.3_linux-arm64.tar.gz",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("BSD", () => {
|
||||
it("should return vanilla tar.gz for freebsd x64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "freebsd" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_0.154.3_freebsd-amd64.tar.gz",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return vanilla tar.gz for openbsd x64", () => {
|
||||
Object.defineProperty(process, "platform", { value: "openbsd" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBe(
|
||||
"hugo_0.154.3_openbsd-amd64.tar.gz",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unsupported platforms", () => {
|
||||
it("should return null for unsupported platform", () => {
|
||||
Object.defineProperty(process, "platform", { value: "sunos" });
|
||||
Object.defineProperty(process, "arch", { value: "x64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for unsupported arch on linux", () => {
|
||||
Object.defineProperty(process, "platform", { value: "linux" });
|
||||
Object.defineProperty(process, "arch", { value: "ia32" });
|
||||
expect(getReleaseFilename("0.154.3")).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null for unsupported arch on freebsd", () => {
|
||||
Object.defineProperty(process, "platform", { value: "freebsd" });
|
||||
Object.defineProperty(process, "arch", { value: "arm64" });
|
||||
expect(getReleaseFilename("0.154.3")).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getChecksumFilename", () => {
|
||||
it("should return correct checksum filename", () => {
|
||||
expect(getChecksumFilename("0.154.3")).toBe("hugo_0.154.3_checksums.txt");
|
||||
});
|
||||
|
||||
it("should handle different version formats", () => {
|
||||
expect(getChecksumFilename("1.0.0")).toBe("hugo_1.0.0_checksums.txt");
|
||||
expect(getChecksumFilename("0.100.0")).toBe("hugo_0.100.0_checksums.txt");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isExtended", () => {
|
||||
it("should return true for extended releases", () => {
|
||||
expect(isExtended("hugo_extended_0.154.3_linux-amd64.tar.gz")).toBe(true);
|
||||
expect(isExtended("hugo_extended_0.154.3_darwin-universal.pkg")).toBe(
|
||||
true,
|
||||
);
|
||||
expect(isExtended("hugo_extended_0.154.3_windows-amd64.zip")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for vanilla releases", () => {
|
||||
expect(isExtended("hugo_0.154.3_windows-arm64.zip")).toBe(false);
|
||||
expect(isExtended("hugo_0.154.3_freebsd-amd64.tar.gz")).toBe(false);
|
||||
expect(isExtended("hugo_0.154.3_openbsd-amd64.tar.gz")).toBe(false);
|
||||
expect(getReleaseFilename("0.163.3")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
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"),
|
||||
).toBe(
|
||||
"https://github.com/gohugoio/hugo/releases/download/v0.154.3/hugo_extended_0.154.3_linux-amd64.tar.gz",
|
||||
);
|
||||
});
|
||||
|
||||
it("should work with checksum files", () => {
|
||||
expect(getReleaseUrl("0.154.3", "hugo_0.154.3_checksums.txt")).toBe(
|
||||
"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",
|
||||
it("returns the GitHub release URL", () => {
|
||||
expect(getReleaseUrl("0.163.3", "hugo_extended_0.163.3_linux-amd64.tar.gz")).toBe(
|
||||
"https://github.com/gohugoio/hugo/releases/download/v0.163.3/hugo_extended_0.163.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("getChecksumFilename", () => {
|
||||
it("returns the checksums filename", () => {
|
||||
expect(getChecksumFilename("0.163.3")).toBe("hugo_0.163.3_checksums.txt");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBinPath with HUGO_BIN_PATH", () => {
|
||||
describe("isExtended", () => {
|
||||
it("identifies Extended release filenames", () => {
|
||||
expect(isExtended("hugo_extended_0.163.3_linux-amd64.tar.gz")).toBe(true);
|
||||
expect(isExtended("hugo_0.163.3_windows-arm64.zip")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPlatformPackageBinaryPath", () => {
|
||||
it("resolves a package binary subpath", () => {
|
||||
const pkg = getPlatformPackage("linux", "x64");
|
||||
if (!pkg) throw new Error("Expected Linux x64 package");
|
||||
|
||||
const resolved = getPlatformPackageBinaryPath(pkg, (specifier) => {
|
||||
expect(specifier).toBe("@jakejarvis/hugo-extended-linux-amd64/bin/hugo");
|
||||
return "/node_modules/@jakejarvis/hugo-extended-linux-amd64/bin/hugo";
|
||||
});
|
||||
|
||||
expect(resolved).toBe("/node_modules/@jakejarvis/hugo-extended-linux-amd64/bin/hugo");
|
||||
});
|
||||
|
||||
it("returns null when the optional package is missing", () => {
|
||||
const pkg = getPlatformPackage("linux", "x64");
|
||||
if (!pkg) throw new Error("Expected Linux x64 package");
|
||||
|
||||
const resolved = getPlatformPackageBinaryPath(pkg, () => {
|
||||
const error = new Error("missing") as NodeJS.ErrnoException;
|
||||
error.code = "MODULE_NOT_FOUND";
|
||||
throw error;
|
||||
});
|
||||
|
||||
expect(resolved).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBinPath", () => {
|
||||
let originalBinPath: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -384,83 +195,37 @@ describe("utils", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("should return custom path when HUGO_BIN_PATH is set", () => {
|
||||
it("returns HUGO_BIN_PATH when 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)?$/);
|
||||
expect(getBinPath()).toBe("/custom/path/to/hugo");
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
it("logs info messages", () => {
|
||||
logger.info("test message");
|
||||
expect(console.info).toHaveBeenCalledWith("test message");
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
it("logs warning messages", () => {
|
||||
logger.warn("warning message");
|
||||
expect(console.warn).toHaveBeenCalledWith("⚠ warning message");
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
it("logs error messages", () => {
|
||||
logger.error("error message");
|
||||
expect(console.error).toHaveBeenCalledWith("✖ error message");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+6
-9
@@ -3,25 +3,22 @@
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"types": ["node"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"verbatimModuleSyntax": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": false,
|
||||
"sourceMap": false,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"resolveJsonModule": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["dist", "dist-platforms", "node_modules"]
|
||||
}
|
||||
|
||||
+1
-3
@@ -4,9 +4,7 @@ export default defineConfig({
|
||||
entry: ["src/**/*.ts"],
|
||||
outDir: "dist",
|
||||
clean: true,
|
||||
dts: {
|
||||
resolve: true,
|
||||
},
|
||||
dts: true,
|
||||
unbundle: true,
|
||||
copy: ["src/generated"],
|
||||
});
|
||||
|
||||
+1
-6
@@ -16,12 +16,7 @@ export default defineConfig({
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
include: ["src/**/*.ts"],
|
||||
exclude: [
|
||||
"src/generated/**",
|
||||
"src/**/*.d.ts",
|
||||
"**/*.test.ts",
|
||||
"scripts/**",
|
||||
],
|
||||
exclude: ["src/generated/**", "src/**/*.d.ts", "**/*.test.ts", "scripts/**"],
|
||||
},
|
||||
|
||||
// Timeout for integration tests (some may be slow)
|
||||
|
||||
Reference in New Issue
Block a user