Skip to content
Merged
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,13 @@ argument required in the third position:

Since by default Envalid's output is wrapped in a Proxy, structuredClone [will not work](https://bugzilla.mozilla.org/show_bug.cgi?id=1269327#c1) on it. See [#177](https://github.com/af/envalid/issues/177).

### Can I use Envalid outside Node (e.g. QuickJS)?

Yes. [WinterTC](https://min-common-api.proposal.wintertc.org/)-compatible runtimes
already provide `URL` and `console` (including `console.error`). Minimal hosts that
do not may need a small shim — see [docs/runtime-globals.md](docs/runtime-globals.md)
and [#253](https://github.com/af/envalid/issues/253).

## Related projects

- [dotenv](https://www.npmjs.com/package/dotenv) is a very handy tool for loading env vars from
Expand Down
63 changes: 63 additions & 0 deletions docs/runtime-globals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Runtime globals (WinterTC and minimal hosts)

Envalid expects a few web-platform globals:

- `URL` — when you use the built-in `url()` validator
- `console` / `console.error` — when you use the default reporter

[WinterTC’s Minimum Common Web API](https://min-common-api.proposal.wintertc.org/)
requires both `URL` and `globalThis.console`. The `console` object is defined by the
[WHATWG Console Standard](https://console.spec.whatwg.org/), which includes
`console.error`. Runtimes that aim to be web-interoperable (Node, Bun, Deno,
browsers, Cloudflare Workers, …) already provide these, so no shim is needed.

Smaller engines that do **not** target that baseline — for example Alpine
[QuickJS](https://bellard.org/quickjs/) (`qjs`) used to validate env at container
entrypoint without shipping Node — may be missing pieces. See
[#253](https://github.com/af/envalid/issues/253).

## When Envalid needs them

| Global | When needed |
| --- | --- |
| `console.error` | Default reporter (at failure). Not needed if you pass your own `reporter` / `null`. |
| `URL` | Only if you use the built-in `url()` validator (`new URL(x)`). |

Importing `envalid` itself does not require these globals (the default logger binds
`console.error` lazily). They must exist before you call `cleanEnv` with `url()`,
and before the default reporter runs.

## Workable shim (QuickJS)

A copy-paste starting point for Alpine QuickJS lives at
[`example/quickjs-host-shim.mjs`](../example/quickjs-host-shim.mjs). Apply it
before `cleanEnv` / `url()` if your host is missing pieces (it need not run before
`import 'envalid'`). Adapt the `console` and `URL` stubs to your engine (other
hosts can no-op or map to `print` / native logging instead of QuickJS `std`).

The shim fills `console.*` for the default reporter; you can omit that part if you
always pass a custom `reporter`.

```js
import { cleanEnv, str, url } from 'envalid'
import './quickjs-host-shim.mjs' // before cleanEnv / url(); order vs envalid import does not matter
import * as std from 'std'

const env = cleanEnv(
/* your env object */,
{ API_URL: url(), NAME: str() },
{
// Custom reporter: default throws outside Node; std.exit fails closed at entrypoint
reporter: ({ errors }) => {
std.err.puts('Invalid environment:\n' + Object.keys(errors).join(', ') + '\n')
std.exit(1)
},
},
)
```

## Notes

- Envalid does **not** ship a QuickJS stdlib integration or a full URL implementation.
- With a custom reporter, you typically only need a `URL` polyfill when using `url()`.
- Default reporter still calls `console.error` and, in Node, `process.exit(1)`.
36 changes: 36 additions & 0 deletions example/quickjs-host-shim.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Minimal host patches for Alpine QuickJS (`qjs`) when using envalid.
* Apply before cleanEnv / url() (not required before import envalid).
* See docs/runtime-globals.md and https://github.com/af/envalid/issues/253
*/
import * as std from 'std'

const root = globalThis

const log = (...args) => {
std.err.puts(args.map(String).join(' ') + '\n')
}

const consoleRef = typeof root.console === 'object' && root.console !== null ? root.console : {}
root.console = consoleRef

for (const method of ['error', 'warn', 'info', 'log']) {
if (typeof consoleRef[method] !== 'function') {
consoleRef[method] = log
}
}

if (typeof root.URL !== 'function') {
root.URL = class URL {
constructor(input) {
const value = String(input)
if (!/^[a-zA-Z][a-zA-Z\d+.-]*:\/\/\S+$/.test(value)) {
throw new TypeError('Invalid URL')
}
this.href = value
}
toString() {
return this.href
}
}
}
9 changes: 7 additions & 2 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ export class EnvError extends TypeError {
constructor(message?: string) {
super(message)
Object.setPrototypeOf(this, new.target.prototype)
Error.captureStackTrace(this, EnvError)
// Optional; not on all engines (e.g. QuickJS) — see MDN Error.captureStackTrace
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, EnvError)
}
this.name = this.constructor.name
}
}
Expand All @@ -14,7 +17,9 @@ export class EnvMissingError extends ReferenceError {
constructor(message?: string) {
super(message)
Object.setPrototypeOf(this, new.target.prototype)
Error.captureStackTrace(this, EnvMissingError)
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, EnvMissingError)
}
this.name = this.constructor.name
}
}
5 changes: 4 additions & 1 deletion src/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ type ExtraOptions<T> = {
logger: (output: string) => void
}

const defaultLogger = console.error.bind(console)
// Lazy: importing envalid must not require console.error
const defaultLogger: Logger = (data, ...args) => {
console.error(data, ...args)
}

// Apply ANSI colors to the reporter output only if we detect that we're running in Node
const isNode = !!(typeof process === 'object' && process?.versions?.node)
Expand Down
Loading