Summary
The @expo/config bundled inside EAS CLI cannot read a TypeScript app.config.ts. There are two independent failures, both reproducible from a blank project on eas-cli@22.0.0 and 21.8.0:
- A config that references
import.meta fails with exports is not defined in ES module scope.
- Any TypeScript config fails with
Cannot read properties of undefined (reading 'CommonJS').
Both come from EAS CLI shipping a config-reading stack several SDK generations behind the configs it reads. The bundled copy is only reached after a primary config read has already failed, so its own failure gets appended to an unrelated error. Users then see a second, misleading error blaming an app config that is fine.
Managed or bare?
Managed, though both reproductions below are standalone. They need no Expo project, only an app.config.ts and a package.json.
Environment
OS: macOS 26.5.2
Node: 22.22.2
npm: 10.9.7
expo: 56.0.18 · expo-router: 56.2.15 · react-native: 0.85.3
Expo Workflow: managed
Shipped inside npm i eas-cli@22.0.0, and identical in 21.8.0:
| Package |
Version |
@expo/config (top level) |
55.0.10 |
@expo/config (under @expo/prebuild-config) |
11.0.13 |
@expo/require-utils |
55.0.6 |
typescript |
7.0.2 |
sucrase |
3.35.0 |
An SDK 56 project resolves @expo/config@56.0.13 and @expo/require-utils@56.1.6, and reads the same configs without error.
Steps to reproduce
mkdir eas-config-repro && cd eas-config-repro
npm init -y
npm i eas-cli@22.0.0
Each fixture directory below also needs any package.json, for example { "name": "minimal", "version": "1.2.3" }.
Bug 1: import.meta in a config breaks the bundled fallback
The fallback in getManagedApplicationTargetEntitlementsAsync (build/project/ios/entitlements.js) calls getPrebuildConfigAsync. That resolves @expo/prebuild-config's nested @expo/config@11.0.13, not the 55.0.10 at the top level. Its evalConfig is sucrase.transform(…, { transforms: ['typescript', 'imports'] }) followed by require-from-string.
Sucrase cannot downlevel import.meta, and neither can tsc, so it survives verbatim into the CommonJS output:
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); …
… createRequire.call(void 0, _nullishCoalesce(import.meta.url, () => ( __filename)))
require-from-string then calls module._compile(code, filename) with no format argument, which leaves the decision to Node's module-syntax detection. It sees import.meta, classifies the module as ESM, and the emitted exports.… assignments cannot resolve.
fixture/app.config.ts
import { createRequire } from 'node:module';
import type { ConfigContext, ExpoConfig } from 'expo/config';
const requirePackageJson = createRequire(import.meta.url ?? __filename);
const { version } = requirePackageJson('./package.json') as { version: string };
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: 'Minimal',
slug: 'minimal',
version,
});
// bug1.cjs
const path = require('node:path');
const nested = path.join(__dirname, 'node_modules/@expo/prebuild-config/node_modules/@expo/config');
const { evalConfig } = require(path.join(nested, 'build/evalConfig.js'));
const configFile = path.join(__dirname, 'fixture/app.config.ts');
console.log('loader @expo/config:', require(path.join(nested, 'package.json')).version);
try {
const res = evalConfig(configFile, { projectRoot: path.dirname(configFile), config: {} });
console.log('OK ->', res.config.name);
} catch (error) {
console.log('FAILED ->', error.message.split('\n')[0]);
}
$ node bug1.cjs
loader @expo/config: 11.0.13
FAILED -> exports is not defined in ES module scope
The same fixture loads fine under @expo/config@56.0.13. There, @expo/require-utils@56.x probes its own CommonJS output with containsModuleSyntax() (a vm.compileFunction check) and re-transpiles as ESM when that trips. @expo/config@11.0.13 predates the recovery.
How it looked in a real CI run of eas build --platform ios, after an unrelated config-plugin failure:
Falling back to the version of "@expo/config" shipped with the EAS CLI.
...
The bundled config fallback also failed with: Error reading Expo config at app.config.ts:
exports is not defined in ES module scope
Bug 2: the top-level bundled @expo/config cannot transpile any TypeScript config
@expo/require-utils@55.0.6's loadTypescript() calls require('typescript') inside a try/catch that only handles MODULE_NOT_FOUND. EAS CLI ships typescript@7.0.2, whose "." export is ./lib/version.cjs, a stub with no compiler API:
require('typescript').version // '7.0.2'
typeof require('typescript').transpileModule // 'undefined'
require('typescript').ModuleKind // undefined
The stub is truthy, so it passes the guard, and evalModule then dereferences ts.ModuleKind.CommonJS.
@expo/require-utils@56.1.6 already has the fix, it just has not reached EAS CLI:
if (typeof _ts?.transpileModule !== 'function') {
_ts = null;
return null;
}
fixture2/app.config.ts is an ordinary config, with no import.meta anywhere in it:
import type { ConfigContext, ExpoConfig } from 'expo/config';
export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: 'Minimal',
slug: 'minimal',
});
// bug2.cjs
const path = require('node:path');
const { getConfig } = require(path.join(__dirname, 'node_modules/@expo/config'));
try {
const { exp } = getConfig(path.join(__dirname, 'fixture2'), { skipSDKVersionRequirement: true });
console.log('OK ->', exp.name);
} catch (error) {
console.log('FAILED ->', error.message.split('\n').filter(Boolean).pop());
}
$ node bug2.cjs
FAILED -> Cannot read properties of undefined (reading 'CommonJS')
Suggested fix
Bumping EAS CLI's @expo/config, @expo/require-utils, and @expo/prebuild-config to the SDK 56 line addresses both. 56.1.6 carries the TypeScript 7 guard for bug 2 and the containsModuleSyntax() ESM re-transpile for bug 1. Bug 2 can also be fixed on its own, either by pinning typescript to a 5.x or 6.x line inside EAS CLI, or by dropping the dependency so loadTypescript() falls through to module.stripTypeScriptTypes.
Summary
The
@expo/configbundled inside EAS CLI cannot read a TypeScriptapp.config.ts. There are two independent failures, both reproducible from a blank project oneas-cli@22.0.0and21.8.0:import.metafails withexports is not defined in ES module scope.Cannot read properties of undefined (reading 'CommonJS').Both come from EAS CLI shipping a config-reading stack several SDK generations behind the configs it reads. The bundled copy is only reached after a primary config read has already failed, so its own failure gets appended to an unrelated error. Users then see a second, misleading error blaming an app config that is fine.
Managed or bare?
Managed, though both reproductions below are standalone. They need no Expo project, only an
app.config.tsand apackage.json.Environment
Shipped inside
npm i eas-cli@22.0.0, and identical in21.8.0:@expo/config(top level)@expo/config(under@expo/prebuild-config)@expo/require-utilstypescriptsucraseAn SDK 56 project resolves
@expo/config@56.0.13and@expo/require-utils@56.1.6, and reads the same configs without error.Steps to reproduce
Each fixture directory below also needs any
package.json, for example{ "name": "minimal", "version": "1.2.3" }.Bug 1:
import.metain a config breaks the bundled fallbackThe fallback in
getManagedApplicationTargetEntitlementsAsync(build/project/ios/entitlements.js) callsgetPrebuildConfigAsync. That resolves@expo/prebuild-config's nested@expo/config@11.0.13, not the55.0.10at the top level. ItsevalConfigissucrase.transform(…, { transforms: ['typescript', 'imports'] })followed byrequire-from-string.Sucrase cannot downlevel
import.meta, and neither can tsc, so it survives verbatim into the CommonJS output:require-from-stringthen callsmodule._compile(code, filename)with no format argument, which leaves the decision to Node's module-syntax detection. It seesimport.meta, classifies the module as ESM, and the emittedexports.…assignments cannot resolve.fixture/app.config.tsThe same fixture loads fine under
@expo/config@56.0.13. There,@expo/require-utils@56.xprobes its own CommonJS output withcontainsModuleSyntax()(avm.compileFunctioncheck) and re-transpiles as ESM when that trips.@expo/config@11.0.13predates the recovery.How it looked in a real CI run of
eas build --platform ios, after an unrelated config-plugin failure:Bug 2: the top-level bundled
@expo/configcannot transpile any TypeScript config@expo/require-utils@55.0.6'sloadTypescript()callsrequire('typescript')inside atry/catchthat only handlesMODULE_NOT_FOUND. EAS CLI shipstypescript@7.0.2, whose"."export is./lib/version.cjs, a stub with no compiler API:The stub is truthy, so it passes the guard, and
evalModulethen dereferencests.ModuleKind.CommonJS.@expo/require-utils@56.1.6already has the fix, it just has not reached EAS CLI:fixture2/app.config.tsis an ordinary config, with noimport.metaanywhere in it:Suggested fix
Bumping EAS CLI's
@expo/config,@expo/require-utils, and@expo/prebuild-configto the SDK 56 line addresses both. 56.1.6 carries the TypeScript 7 guard for bug 2 and thecontainsModuleSyntax()ESM re-transpile for bug 1. Bug 2 can also be fixed on its own, either by pinningtypescriptto a 5.x or 6.x line inside EAS CLI, or by dropping the dependency soloadTypescript()falls through tomodule.stripTypeScriptTypes.