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
2 changes: 1 addition & 1 deletion docs/pages/more/expo-cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ From here, you can choose to generate basic project files like:
| `EXPO_NO_DOTENV` | **boolean** | <div className="flex items-center pb-1.5"><StatusTag note="SDK 49+" /></div>Prevent all `.env` file loading across Expo CLI. |
| `EXPO_NO_METRO_LAZY` | **boolean** | Prevent adding the `lazy=true` query parameter to Metro URLs (`metro@0.76.3` and greater). This disables `import()` support. |
| `EXPO_USE_TYPED_ROUTES` | **boolean** | Use `expo.experiments.typedRoutes` to enable statically typed routes in Expo Router. |
| `EXPO_METRO_UNSTABLE_ERRORS` | **boolean** | <div className="flex items-center pb-1.5"><StatusTag status="experimental" /></div>Enable unstable error message improvements in Metro bundler. The features behind this flag are subject to removal and may be upstreamed. |
| ~~`EXPO_METRO_UNSTABLE_ERRORS`~~ | **boolean** | <div className="flex items-center pb-1.5"><StatusTag status="deprecated" /></div>Disable inverse dependency stack trace for Metro bundling errors. Enabled by default. |
| ~~`EXPO_USE_METRO_WORKSPACE_ROOT`~~ | **boolean** | <div className="flex items-center pb-1.5"><StatusTag status="deprecated" note="SDK 52+" /></div>Enable auto server root detection for Metro. This will change the server root to the workspace root. Useful for monorepos. |
| `EXPO_NO_METRO_WORKSPACE_ROOT` | **boolean** | <div className="flex items-center pb-1.5"><StatusTag note="SDK 52+" /></div>Disable auto server root detection for Metro. Disabling will not change the server root to the workspace root. Enabling this is useful for monorepos. |
| ~~`EXPO_USE_UNSTABLE_DEBUGGER`~~ | **boolean** | <div className="flex items-center pb-1.5"><StatusTag status="deprecated" note="SDK 52+" /></div>Enable the experimental debugger from React Native. |
Expand Down
3 changes: 3 additions & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
- Support SSR imports of internal node builtins such as `_http_agent`. ([#37494](https://github.com/expo/expo/pull/37494) by [@EvanBacon](https://github.com/EvanBacon))
- Allow anonymous sessions even when `projectId` is set ([#36874](https://github.com/expo/expo/pull/36874) by [@kadikraman](https://github.com/kadikraman))
- Add static rewrites support to export and server-side handling ([#37930](https://github.com/expo/expo/pull/37930) by [@hassankhan](https://github.com/hassankhan))
- Fix missing Error import stack with `EXPO_METRO_UNSTABLE_ERRORS` enabled and no cause ([#38256](https://github.com/expo/expo/pull/38256)) by [@krystofwoldrich](https://github.com/krystofwoldrich))
- Enable inverse dependency stack trace (`EXPO_METRO_UNSTABLE_ERRORS`) for Metro bundling errors by default ([#38296](https://github.com/expo/expo/pull/38296) by [@krystofwoldrich](https://github.com/krystofwoldrich))
- Fix duplicate code frames printed in transformation error ([#38288](https://github.com/expo/expo/pull/38288) by [@krystofwoldrich](https://github.com/krystofwoldrich))

### 💡 Others

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,12 @@ import {
import { createRouteHandlerMiddleware } from './createServerRouteMiddleware';
import { ExpoRouterServerManifestV1, fetchManifest } from './fetchRouterManifest';
import { instantiateMetroAsync } from './instantiateMetro';
import { getErrorOverlayHtmlAsync, IS_METRO_BUNDLE_ERROR_SYMBOL } from './metroErrorInterface';
import {
attachImportStackToRootMessage,
dropStackIfContainsCodeFrame,
getErrorOverlayHtmlAsync,
IS_METRO_BUNDLE_ERROR_SYMBOL,
} from './metroErrorInterface';
import { metroWatchTypeScriptFiles } from './metroWatchTypeScriptFiles';
import {
getRouterDirectoryModuleIdWithManifest,
Expand Down Expand Up @@ -1571,14 +1576,8 @@ export class MetroBundlerDevServer extends BundlerDevServer {
revision = props.revision;
}
} catch (error) {
if (error instanceof Error) {
// Space out build failures.
const cause = error.cause as undefined | { _expoImportStack?: string };
if (cause && '_expoImportStack' in cause) {
error.message += '\n\n' + cause._expoImportStack;
}
}

attachImportStackToRootMessage(error);
dropStackIfContainsCodeFrame(error);
throw error;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
maybeSymbolicateAndFormatReactErrorLogAsync,
parseErrorStringToObject,
} from '../serverLogLikeMetro';
import { attachImportStackToRootMessage, nearestImportStack } from './metroErrorInterface';

const debug = require('debug')('expo:metro:logger') as typeof console.log;

Expand Down Expand Up @@ -234,17 +235,13 @@ export class MetroTerminalReporter extends TerminalReporter {

_logBundlingError(error: SnippetError): void {
const moduleResolutionError = formatUsingNodeStandardLibraryError(this.projectRoot, error);
const cause = error.cause as undefined | { _expoImportStack?: string };
if (moduleResolutionError) {
let message = maybeAppendCodeFrame(moduleResolutionError, error.message);
if (cause?._expoImportStack) {
message += `\n\n${cause?._expoImportStack}`;
}
message += '\n\n' + nearestImportStack(error);
return this.terminal.log(message);
}
if (cause?._expoImportStack) {
error.message += `\n\n${cause._expoImportStack}`;
}

attachImportStackToRootMessage(error);
return super._logBundlingError(error);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import {
attachImportStackToRootMessage,
nearestImportStack,
likelyContainsCodeFrame,
dropStackIfContainsCodeFrame,
} from '../metroErrorInterface';

type ErrorWithImportStack = Error & {
_expoImportStack?: string;
cause?: ErrorWithImportStack;
};

describe('attachImportStackToRootMessage', () => {
it('no change to error', () => {
const actual = new Error('Test error');
attachImportStackToRootMessage(actual);

expect(actual).toEqual(new Error('Test error'));
expect(actual.stack).toBeDefined();
});

it('import from root', () => {
const actual: ErrorWithImportStack = new Error('Test error');
actual._expoImportStack = `
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`;
attachImportStackToRootMessage(actual);

expect(actual.stack).toBeUndefined();
});

it('import from root', () => {
const actual: ErrorWithImportStack = new Error('Test error');
actual._expoImportStack = `
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`;
attachImportStackToRootMessage(actual);

expect(actual.message).toEqual(`Test error


Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`);
});

it('import from direct cause', () => {
const actual: ErrorWithImportStack = new Error('Test error');
actual.cause = new Error('Direct cause');
actual.cause._expoImportStack = `
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`;
attachImportStackToRootMessage(actual);

expect(actual.message).toEqual(`Test error


Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`);
});

it('import from cause chain', () => {
const actual: ErrorWithImportStack = new Error('Test error');
actual.cause = new Error('Direct cause');
actual.cause.cause = new Error('Indirect cause');
actual.cause.cause._expoImportStack = `
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`;
attachImportStackToRootMessage(actual);

expect(actual.message).toEqual(`Test error


Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`);
});

it('import from nearest cause in chain', () => {
const actual: ErrorWithImportStack = new Error('Test error');
actual.cause = new Error('Direct cause');
actual.cause.cause = new Error('Indirect cause');
actual.cause.cause.cause = new Error('Another indirect cause');
actual.cause.cause._expoImportStack = `
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`;
actual.cause.cause.cause._expoImportStack = `
Import stack:
hooks/hooks/useApples.ts
| import "not-existing-module-2"`;
attachImportStackToRootMessage(actual);

expect(actual.message).toEqual(`Test error


Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`);
});
});

describe('nearestImportStack', () => {
it('returns undefined for non-error', () => {
expect(nearestImportStack('not an error')).toBeUndefined();
});

it('returns undefined when no import stack exists', () => {
expect(nearestImportStack(new Error('Test error'))).toBeUndefined();
});

it('returns import stack from root error', () => {
const error: ErrorWithImportStack = new Error('Test error');
error._expoImportStack = `
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`;

expect(nearestImportStack(error)).toEqual(`
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`);
});

it('returns import stack from direct cause', () => {
const error: ErrorWithImportStack = new Error('Test error');
error.cause = new Error('Direct cause') as ErrorWithImportStack;
error.cause._expoImportStack = `
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`;

expect(nearestImportStack(error)).toEqual(`
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`);
});

it('returns import stack from deeper in cause chain', () => {
const error: ErrorWithImportStack = new Error('Test error');
error.cause = new Error('Direct cause') as ErrorWithImportStack;
error.cause.cause = new Error('Indirect cause') as ErrorWithImportStack;
error.cause.cause._expoImportStack = `
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`;

expect(nearestImportStack(error)).toEqual(`
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`);
});

it('returns import stack from nearest cause in chain', () => {
const error: ErrorWithImportStack = new Error('Test error');
error.cause = new Error('Direct cause') as ErrorWithImportStack;
error.cause.cause = new Error('Indirect cause') as ErrorWithImportStack;
error.cause.cause._expoImportStack = `
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`;

// Add another cause with an import stack that should be ignored
error.cause.cause.cause = new Error('Another indirect cause') as ErrorWithImportStack;
error.cause.cause.cause._expoImportStack = `
Import stack:
hooks/hooks/useApples.ts
| import "not-existing-module-2"`;

expect(nearestImportStack(error)).toEqual(`
Import stack:
hooks/hooks/useBananas.ts
| import "not-existing-module"`);
});
});

describe('likelyContainsCodeFrame', () => {
it('returns false for undefined', () => {
expect(likelyContainsCodeFrame(undefined)).toBe(false);
});

it('returns false for empty string', () => {
expect(likelyContainsCodeFrame('')).toBe(false);
});

it('returns false for non-code frame message', () => {
expect(likelyContainsCodeFrame('This is a regular error message')).toBe(false);
});

it('returns true for code frame message', () => {
expect(
likelyContainsCodeFrame(`
SyntaxError: hooks/useBananas.ts: Unexpected token (3:9)

2 | import { FruitLabelPrefix } from './useFruit';
> 3 | import { [] } from './useFruit';
| ^
4 |
5 | export function useBananas() {
`)
).toBe(true);
});

it('returns true for code frame with ANSI colors', () => {
expect(
likelyContainsCodeFrame(`
\x1b[31mSyntaxError: hooks/useBananas.ts: Unexpected token (3:9)\x1b[0m

\x1b[90m 2 |\x1b[0m import { FruitLabelPrefix } from './useFruit';
\x1b[31m> 3 |\x1b[0m import { [] } from './useFruit';
\x1b[31m |\x1b[0m \x1b[31m^\x1b[0m
\x1b[90m 4 |\x1b[0m
\x1b[90m 5 |\x1b[0m export function useBananas() {
`)
).toBe(true);
});
});

describe('dropStackIfContainsCodeFrame', () => {
it('keeps stack if no code frame', () => {
const error = new Error('This is a regular error message');
const originalStack = error.stack;
dropStackIfContainsCodeFrame(error);
expect(error.stack).toEqual(originalStack);
});

it('drops stack if code frame is present', () => {
const error = new Error(`
\x1b[31mSyntaxError: hooks/useBananas.ts: Unexpected token (3:9)\x1b[0m

\x1b[90m 2 |\x1b[0m import { FruitLabelPrefix } from './useFruit';
\x1b[31m> 3 |\x1b[0m import { [] } from './useFruit';
\x1b[31m |\x1b[0m \x1b[31m^\x1b[0m
\x1b[90m 4 |\x1b[0m
\x1b[90m 5 |\x1b[0m export function useBananas() {
`);
dropStackIfContainsCodeFrame(error);
expect(error.stack).toBeUndefined();
});
});
Loading
Loading