diff --git a/.changeset/husky-fallback.md b/.changeset/husky-fallback.md new file mode 100644 index 00000000..7f63319e --- /dev/null +++ b/.changeset/husky-fallback.md @@ -0,0 +1,5 @@ +--- +"nostream": patch +--- + +fix: add husky install fallback for non-dev environments diff --git a/.husky/install.mjs b/.husky/install.mjs new file mode 100644 index 00000000..2f196fc5 --- /dev/null +++ b/.husky/install.mjs @@ -0,0 +1,18 @@ +import { existsSync } from 'node:fs'; +import { execSync } from 'node:child_process'; + +// Skip Husky installation in environments where hooks are not needed +if ( + process.env.NODE_ENV === 'production' || + process.env.CI === 'true' || + process.env.HUSKY === '0' || + !existsSync('.git') +) { + process.exit(0); +} + +try { + execSync('npx husky install', { stdio: 'ignore' }); +} catch { + process.exit(0); +} diff --git a/package.json b/package.json index dd74f3ab..8053eb69 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "docker:cover:integration": "pnpm run docker:integration:run pnpm exec nyc --report-dir .coverage/integration pnpm run test:integration -- -p cover", "postdocker:integration:run": "docker compose -f ./test/integration/docker-compose.yml down", "prepack": "pnpm run build", - "prepare": "husky install || exit 0", + "prepare": "test -f .husky/install.mjs && node .husky/install.mjs || true", "changeset:version": "changeset version && pnpm install --lockfile-only", "changeset:publish": "changeset publish" }, diff --git a/test/unit/husky/install.spec.ts b/test/unit/husky/install.spec.ts new file mode 100644 index 00000000..210d1361 --- /dev/null +++ b/test/unit/husky/install.spec.ts @@ -0,0 +1,57 @@ +import { expect } from 'chai' +import fs from 'fs' +import os from 'os' +import path from 'path' +import { spawnSync } from 'child_process' + +const projectRoot = process.cwd() +const installScriptPath = path.join(projectRoot, '.husky', 'install.mjs') + +const runInstall = (cwd: string, env: NodeJS.ProcessEnv = {}) => { + return spawnSync('node', [installScriptPath], { + cwd, + env: { + ...process.env, + ...env, + }, + encoding: 'utf-8', + timeout: 10_000, + }) +} + +describe('husky install script', () => { + it('exits successfully when .git is missing', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nostream-husky-no-git-')) + + try { + const result = runInstall(tmpDir) + expect(result.status).to.equal(0) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('exits successfully when HUSKY is disabled', () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nostream-husky-disabled-')) + + try { + const result = runInstall(tmpDir, { HUSKY: '0' }) + expect(result.status).to.equal(0) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) + + it('exits successfully when husky package is unavailable even if .git exists', function () { + this.timeout(15_000) + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nostream-husky-missing-package-')) + + try { + fs.mkdirSync(path.join(tmpDir, '.git')) + const result = runInstall(tmpDir) + expect(result.status).to.equal(0) + } finally { + fs.rmSync(tmpDir, { recursive: true, force: true }) + } + }) +})