Repository navigation
Bundled @expo/config cannot read a TypeScript app.config.ts (two independent failures in eas-cli 22.0.0) #4205
Description
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.metafails withexports 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.0Each 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 // undefinedThe 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.
Activity
github-actions commentedon Sep 15, 2026
This issue is stale because it has been open for 30 days with no activity. If there is no activity in the next 7 days, the issue will be closed.
maencharacter commentedon Sep 21, 2026
Bump because it has an open PR
github-actions commentedon Oct 23, 2026
This issue is stale because it has been open for 30 days with no activity. If there is no activity in the next 7 days, the issue will be closed.
erenbati commentedon Oct 24, 2026
Progress on this issue: the fix is still under review in PR #4395. I updated its branch to current
main(mergeable, no conflicts) and added regression tests for both TypeScriptapp.config.tsfailures described here, includingimport.metathrough the prebuild-config reader. All 30 focused Jest tests and the EAS CLI TypeScript check passed locally. Upstream GitHub Actions currently showaction_requiredpending workflow authorization, not test failures. Since the fix is still open for review, please keep #4205 open rather than closing it as stale.