expo / expo/eas-cli

Bundled @expo/config cannot read a TypeScript app.config.ts (two independent failures in eas-cli 22.0.0)

Open
#4,205 1 comment 0 reactions 0 assignees View on GitHub
stale
Dominant language
TypeScript
Stars
1.4k
Forks
236
Avg merge
3d 1h
Merged PRs (30d)
91

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`:

1. A config that references `import.meta` fails with `exports is not defined in ES module scope`.
2. 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

```sh
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:

```js
"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

```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,
});
```

```js
// 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:

```js
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:

```js
if (typeof _ts?.transpileModule !== 'function') {
_ts = null;
return null;
}
```

`fixture2/app.config.ts` is an ordinary config, with no `import.meta` anywhere in it:

```ts
import type { ConfigContext, ExpoConfig } from 'expo/config';

export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: 'Minimal',
slug: 'minimal',
});
```

```js
// 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`.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.