vercel/hyper · error · Error

Error reading configuration: `module.exports` not set

Error message

Error reading configuration: `module.exports` not set

What it means

Thrown by Hyper's config loader (_extract in app/config/init.ts) when it runs the user's `~/.hyper.js` as a vm.Script inside a sandbox exposing a fresh `module` object, and after execution `module.exports` is still falsy. The file is expected to be CommonJS that assigns `module.exports = {config: {...}, keymaps: ..., plugins: [...]}`. If the script is valid syntactically (it passed _syntaxValidation) but never performs that assignment, the loader cannot read a config and aborts. Note: a syntax error does NOT trigger this — _syntaxValidation catches those, notifies, and returns undefined, which then makes _extract's optional-chained `script?.runInNewContext` a no-op, leaving module.exports undefined, so the error can also follow an earlier syntax failure.

Source

Thrown at app/config/init.ts:13

import vm from 'vm';

import merge from 'lodash/merge';

import type {parsedConfig, rawConfig, configOptions} from '../../typings/config';
import notify from '../notify';
import mapKeys from '../utils/map-keys';

const _extract = (script?: vm.Script): Record<string, any> => {
  const module: Record<string, any> = {};
  script?.runInNewContext({module}, {displayErrors: true});
  if (!module.exports) {
    throw new Error('Error reading configuration: `module.exports` not set');
  }
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
  return module.exports;
};

const _syntaxValidation = (cfg: string) => {
  try {
    return new vm.Script(cfg, {filename: '.hyper.js'});
  } catch (_err) {
    const err = _err as {name: string};
    notify(`Error loading config: ${err.name}`, JSON.stringify(err), {error: err});
  }
};

const _extractDefault = (cfg: string) => {
  return _extract(_syntaxValidation(cfg));
};

View on GitHub (pinned to da0c401d7f)

Solutions

  1. Open `~/.hyper.js` and ensure it contains exactly one CommonJS export: `module.exports = { config: {}, keymaps: {}, plugins: [], localPlugins: [] };`.
  2. If the file is empty or unrecognizable, replace its contents with the default from app/config/config-default.json (or run `hyper` to regenerate the default).
  3. Remove any `export default` / `import` ESM syntax — Hyper's config is CommonJS evaluated in a vm sandbox, not bundled.
  4. Check for an earlier syntax error: a desktop notification 'Error loading config: SyntaxError' precedes this throw; fix that first because _syntaxValidation returning undefined is what makes _extract skip execution.
  5. Validate the file with `node -c ~/.hyper.js` (syntax check) and `node -e "const m={}; require('vm').runInNewContext(require('fs').readFileSync(process.env.HOME+'/.hyper.js','utf8'),{module:m}); console.log(typeof m.exports)"` to confirm it logs 'object'.

Example fix

// before — ~/.hyper.js (broken)
// only wrote the object literal, no export
{
  config: { fontSize: 12 }
}
// -> Error reading configuration: `module.exports` not set

// after — ~/.hyper.js (fixed, CommonJS export)
module.exports = {
  config: {
    fontSize: 12,
    fontFamily: 'Menlo, monospace',
  },
  plugins: [],
  localPlugins: [],
  keymaps: {},
};
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate the user config string BEFORE running it through _extract/_extractDefault.
// Mirror the loader's own contract: must compile as a script AND assign module.exports.
import vm from 'vm';

function isAssignableConfig(cfg: string): boolean {
  if (!cfg || !cfg.trim()) return false;
  let script: vm.Script;
  try {
    script = new vm.Script(cfg, {filename: '.hyper.js'});
  } catch {
    return false; // syntax error — _syntaxValidation would notify and return undefined
  }
  const module: Record<string, any> = {};
  try {
    script.runInNewContext({module}, {displayErrors: false});
  } catch {
    return false; // runtime error during assignment
  }
  return module.exports != null && typeof module.exports === 'object';
}

// usage before _extractDefault
if (!isAssignableConfig(rawUserCfgString)) {
  notify('Configuration file is incomplete', 'Recreating ~/.hyper.js with defaults');
  rawUserCfgString = defaultCfgString;
}

Type guard

// Narrow the extracted object after a successful _extract call.
// _extract already guarantees module.exports is truthy; this guard validates shape.
import type { rawConfig } from '../../typings/config';

function isRawConfig(v: unknown): v is rawConfig {
  if (typeof v !== 'object' || v === null) return false;
  const r = v as Record<string, unknown>;
  // config is optional at this layer but if present must be an object
  if ('config' in r && (typeof r.config !== 'object' || r.config === null)) return false;
  if ('plugins' in r && !Array.isArray(r.plugins)) return false;
  if ('localPlugins' in r && !Array.isArray(r.localPlugins)) return false;
  if ('keymaps' in r && (typeof r.keymaps !== 'object' || r.keymaps === null)) return false;
  return true;
}

const extracted = _extract(script);
if (!isRawConfig(extracted)) {
  throw new Error('Configuration shape invalid after extraction');
}

Try / catch

// _extract throws synchronously. Wrap _extractDefault / _init callers so a broken
// user config never takes down app boot — fall back to defaults and notify.
import {_init, _extractDefault} from './config/init';
import defaultRaw from './config/config-default.json';

function safeInit(userRawString: string, defaultRawString: string) {
  let userCfg;
  try {
    userCfg = _extractDefault(userRawString);
  } catch (err) {
    const e = err as Error;
    if (/module\.exports not set/i.test(e.message)) {
      notify('Configuration unreadable', 'Using defaults until ~/.hyper.js sets module.exports');
      userCfg = _extractDefault(defaultRawString); // guaranteed-valid default
    } else {
      throw e; // unknown failure — do not swallow
    }
  }
  return _init(userCfg, _extractDefault(defaultRawString));
}

Prevention

When it happens

Trigger: Loading a `~/.hyper.js` that (a) is empty or whitespace-only, (b) defines `config = {...}` as a free variable instead of assigning to `module.exports`, (c) uses ES-module syntax (`export default {...}`) which is valid as a vm.Script token sequence but does not set module.exports, (d) only contains comments, or (e) had a syntax error caught by _syntaxValidation (returns undefined) so the script never runs and module.exports stays undefined.

Common situations: User hand-edited `.hyper.js` and deleted the `module.exports =` portion; user followed an ES-modules tutorial and wrote `export default`; a config-migration or theme-install snippet wrote bare object literals; editor saved a partial file during crash; new install where the file was created empty by a setup step; syntax error earlier in the file (caught silently by _syntaxValidation, then this error surfaces on the subsequent extract).


AI-assisted analysis of vercel/hyper@da0c401d7f (2026-08-12). Data as JSON: /api/errors/7168ada85389410d. Report an issue: GitHub.