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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion guides/dotenv-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,43 @@ If the target file does not exist anywhere under `paths`, but a template exists

- `templateExtension: 'template'` by default, producing patterns like `.env.local.template`.

If neither the target nor the template exists under any provided path, `editDotenvFile` throws.
If neither the target nor the template exists under any provided path, `editDotenvFile` throws, and the error message lists every candidate filename it checked.

### Shared env template fallback (`scope: 'env'` only)

Env-scoped templates are usually identical across environments (`dev`, `stg`, `prod`, ...), which means keeping separate `.env.<env>.local.template` files per environment just invites drift. When an env-scoped target is missing *and* it has no env-specific template, `resolveDotenvTarget` falls back to a **shared env template** before throwing.

The shared template filename is built the same way as the env target, but with a fixed token in place of the actual env name. That token is configurable via `sharedEnvTemplateToken` (default `'env'`), and it respects `dotenvToken`, `privateToken`, and `templateExtension`:

- `.env.<env>.local` → shared template `.env.env.local.template`
- `.env.<env>` → shared template `.env.env.template`

Resolution order per path, in the configured `searchOrder`:

1. The target itself (e.g. `.env.dev.local`) — if it exists anywhere under `paths`, it wins outright.
2. The env-specific template (e.g. `.env.dev.local.template`) — this **always** takes priority over the shared template when present, in any searched path.
3. Only when neither of the above is found anywhere under `paths`: the shared env template (e.g. `.env.env.local.template`), searched again across the same ordered paths.
Comment on lines +161 to +165

Notes:

- This fallback only applies to `scope: 'env'`. Global-scoped targets (`.env`, `.env.local`) never use it — `.env.local.template` remains the template for `.env.local` only.
- When a shared template is selected, `resolveDotenvTarget` returns the env target path (in the directory where the shared template was found) as `targetPath`, and the shared template path as `templatePath`. `editDotenvFile` bootstraps from it exactly like any other template, reporting `createdFromTemplate: true`.

```ts
import { editDotenvFile } from '@karmaniverous/get-dotenv';

// With `.env.env.local.template` committed at the repo root, this works for
// every env without a per-env template file.
await editDotenvFile(
{ API_URL: 'https://example.com' },
{
paths: ['./'],
scope: 'env',
privacy: 'private',
env: 'dev',
},
);
```

### Example: create `.env.local` from template and set keys

Expand Down
1 change: 1 addition & 0 deletions guides/stan-assistant-guide/editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ await editDotenvFile(
- `searchOrder: 'reverse'` (default): last path wins.
- `searchOrder: 'forward'`: first path wins.
- Template bootstrap: if the target is missing but `<target>.<templateExtension>` exists (default `template`), it is copied first.
- Shared env template fallback (`scope: 'env'` only): if neither the target nor its env-specific template exists, a shared template is tried next. It's named the same way as the env target, but with a fixed token (`sharedEnvTemplateToken`, default `'env'`) in place of the env name — e.g. `.env.env.local.template` for `.env.<env>.local`, or `.env.env.template` for `.env.<env>`. An env-specific template always wins when present. Global scope never uses this fallback.
- Return shape: `{ path, createdFromTemplate, changed }`.

### Selector mapping
Expand Down
1 change: 1 addition & 0 deletions guides/stan-assistant-guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ Notes:
- `searchOrder: 'reverse'` (default): last path wins (highest precedence).
- `searchOrder: 'forward'`: first path wins.
- Template bootstrap: if the selected target is missing but `<target>.<templateExtension>` exists (default extension: `template`), the template is copied first and then edited in place.
- Shared env template fallback (`scope: 'env'` only): if neither the target nor its env-specific template exists anywhere under `paths`, a shared template named with a fixed token in place of the env name (`sharedEnvTemplateToken`, default `'env'`) is tried next — e.g. `.env.env.local.template` for `.env.<env>.local`. An env-specific template always wins over the shared one; global scope never uses this fallback. See [Dotenv editor](../dotenv-editor.md#shared-env-template-fallback-only) for the full resolution order.
- Return shape: `editDotenvFile` returns `{ path, createdFromTemplate, changed }` where `path` is absolute.

Selector mapping (filename construction):
Expand Down
23 changes: 23 additions & 0 deletions src/dotenv/edit/editDotenvFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,29 @@ describe('dotenv/edit/editDotenvFile', () => {
expect(txt).toBe('A=updated\n');
});

it('bootstraps from the shared env template when no env-specific template exists', async () => {
const d = await mk(path.join(ROOT, 'case5', 'p'));
await fs.writeFile(
path.join(d, '.env.env.local.template'),
'A=shared\n',
'utf-8',
);

const res = await editDotenvFile(
{ A: 'updated' },
{
paths: [d],
scope: 'env',
privacy: 'private',
env: 'dev',
},
);
expect(res.createdFromTemplate).toBe(true);
expect(res.path).toBe(path.resolve(d, '.env.dev.local'));
const txt = await fs.readFile(res.path, 'utf-8');
expect(txt).toBe('A=updated\n');
});

it('throws when env-scoped selection is requested without an env', async () => {
const d = await mk(path.join(ROOT, 'case4', 'p'));
await fs.writeFile(path.join(d, '.env'), 'A=1\n', 'utf-8');
Expand Down
3 changes: 3 additions & 0 deletions src/dotenv/edit/editDotenvFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ export async function editDotenvFile(
privacy: options.privacy,
templateExtension,
searchOrder,
...(typeof options.sharedEnvTemplateToken === 'string'
? { sharedEnvTemplateToken: options.sharedEnvTemplateToken }
: {}),
});

let createdFromTemplate = false;
Expand Down
Loading
Loading