Skip to content

Commit a1f567d

Browse files
committed
fix(loader): resolve --config relative paths against cwd, not shipnode's dist/
`shipnode deploy --config ./shipnode.frontend.config.ts` threw `Cannot find module` because jiti's anchor is shipnode's own loader file inside dist/, so relative paths resolved there instead of against the user's working directory. Affected anyone with per-app configs in a monorepo. Absolute paths kept working and stay unchanged (`resolve()` returns absolute paths verbatim). Also moves the 2.5.0 content out of [Unreleased] now that it's tagged.
1 parent 8902e1f commit a1f567d

3 files changed

Lines changed: 57 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to `@devalade/shipnode` will be documented here.
44

55
## [Unreleased]
66

7+
### Fixed
8+
- **`--config <relative-path>` now resolves against the user's cwd, not against shipnode's install dir.** Previously `shipnode deploy --config ./shipnode.frontend.config.ts` threw `Cannot find module './shipnode.frontend.config.ts'` because jiti's anchor is shipnode's own loader file inside `dist/`. Affected anyone using per-app configs in monorepos. Absolute paths kept working and are unchanged.
9+
10+
## [2.5.0] - 2026-05-28
11+
712
### Fixed
813
- **Env vars now actually reach the app under PM2 7.x.** PM2's `env_file` option silently failed to inject variables in 7.0.x, so AdonisJS / NestJS / any framework that re-validates env at boot crashed with "Missing environment variable" after deploy. Each PM2 app is now started as `bash -c "set -a && . <shared-env> && set +a && exec <original-command>"`; the `env_file` line is gone from the generated ecosystem. Secrets stay in the chmod-600 shared env file — they're not duplicated into `ecosystem.config.cjs`. See [ADR-0003](docs/adr/0003-source-env-instead-of-pm2-env-file.md).
914
- **`shipnode env` upload now matches the PM2 ecosystem reference.** Upload was hardcoded to `shared/.env`, while the ecosystem referenced `shared/${envFile}`. If you set `.envFile('.env.production')` the names diverged and PM2 couldn't find the file. Upload now writes to `shared/<envFile>`; a `shared/.env` alias is also maintained so external scripts and the workDir-relative `. ./.env` keep working.

src/config/loader.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ async function detectEnvFile(configFile: string, cwd: string): Promise<string> {
3939
}
4040

4141
export async function loadConfig(cwd: string, configPath?: string): Promise<ShipnodeConfig> {
42-
const file = configPath ?? resolve(cwd, 'shipnode.config.ts');
42+
// Resolve against the user's cwd so `--config ./shipnode.frontend.config.ts`
43+
// works the same as `--config shipnode.frontend.config.ts`. Without this,
44+
// jiti's anchor is shipnode's own dist/ dir and relative paths blow up with
45+
// "Cannot find module". `resolve()` returns absolute paths unchanged.
46+
const file = configPath ? resolve(cwd, configPath) : resolve(cwd, 'shipnode.config.ts');
4347
await loadEnvIntoProcess(await detectEnvFile(file, cwd));
4448

4549
const exists = await pathExists(file);

tests/unit/loader.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2+
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
3+
import { tmpdir } from 'node:os';
4+
import { join } from 'node:path';
5+
import { loadConfig } from '../../src/config/loader.js';
6+
7+
const minimalConfig = `
8+
import { shipnode } from '${join(process.cwd(), 'src/config/builder.ts').replace(/\\/g, '/')}';
9+
10+
export default shipnode
11+
.backend()
12+
.ssh({ host: '1.2.3.4', user: 'deploy' })
13+
.deployTo('/var/www/app')
14+
.pm2('myapp')
15+
.build();
16+
`;
17+
18+
describe('loadConfig — --config path resolution', () => {
19+
let dir: string;
20+
21+
beforeAll(async () => {
22+
dir = await mkdtemp(join(tmpdir(), 'shipnode-loader-'));
23+
await writeFile(join(dir, 'shipnode.custom.config.ts'), minimalConfig);
24+
});
25+
26+
afterAll(async () => {
27+
await rm(dir, { recursive: true, force: true });
28+
});
29+
30+
it('resolves a relative --config path against cwd, not against shipnode dist/', async () => {
31+
// Previously this threw "Cannot find module './shipnode.custom.config.ts'"
32+
// because jiti's anchor is the loader file inside dist/.
33+
const config = await loadConfig(dir, './shipnode.custom.config.ts');
34+
expect(config.app).toBe('backend');
35+
expect(config.remotePath).toBe('/var/www/app');
36+
});
37+
38+
it('accepts a bare filename as --config (no ./ prefix)', async () => {
39+
const config = await loadConfig(dir, 'shipnode.custom.config.ts');
40+
expect(config.pm2?.apps[0].name).toBe('myapp');
41+
});
42+
43+
it('still accepts an absolute --config path', async () => {
44+
const config = await loadConfig(dir, join(dir, 'shipnode.custom.config.ts'));
45+
expect(config.ssh.host).toBe('1.2.3.4');
46+
});
47+
});

0 commit comments

Comments
 (0)