Skip to content
This repository was archived by the owner on May 4, 2026. It is now read-only.
Draft
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
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-10-24 - Node.js `readFileSync` Memory Optimization
**Learning:** In Node.js components, calling `.toString()` on a `Buffer` returned by `fs.readFileSync` or `memfs` `vol.readFileSync` causes an unnecessary intermediate memory allocation. Passing the encoding string (e.g., `'utf8'`) directly as the second argument to `readFileSync` avoids this overhead and returns a string directly.
**Action:** When working with file reads in tests and CLI utilities, refactor `readFileSync(path).toString()` to `readFileSync(path, 'utf8')`. When doing this in TypeScript with `memfs` (`vol`), remember to add `as string` if the type signature demands it, and be prepared to run `jest -u` to update any snapshot tests that might subtly change due to the new string formatting.
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ describe('FileHookTransform', () => {

it('should call hook function from createFingerprintAsync', async () => {
vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
const packageJson = JSON.parse(vol.readFileSync('/app/package.json', 'utf8').toString());
const packageJson = JSON.parse(vol.readFileSync('/app/package.json', 'utf8'));
jest.doMock('/app/package.json', () => packageJson, { virtual: true });
const options = await normalizeOptionsAsync('/app', { fileHookTransform: mockHook });
await createFingerprintAsync('/app', options);
Expand All @@ -121,7 +121,7 @@ describe('FileHookTransform', () => {
}
) as jest.MockedFunction<FileHookTransformFunction>;
vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
const packageJson = JSON.parse(vol.readFileSync('/app/package.json', 'utf8').toString());
const packageJson = JSON.parse(vol.readFileSync('/app/package.json', 'utf8'));
jest.doMock('/app/package.json', () => packageJson, { virtual: true });
const options = await normalizeOptionsAsync('/app', { fileHookTransform: mockExpoConfigHook });
const result = await createFingerprintAsync('/app', options);
Expand Down
79 changes: 77 additions & 2 deletions packages/@expo/fingerprint/src/__tests__/Fingerprint-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,36 @@ describe(diffFingerprintChangesAsync, () => {

expect(normalizedDiff).toMatchInlineSnapshot(`
[
{
"addedSource": {
"contents": "{"extraDependencies":[],"coreFeatures":[],"modules":[]}",
"debugInfo": {
"hash": "10c4144650e3af1f596683ac4ae7a6fd971f7447",
},
"hash": "10c4144650e3af1f596683ac4ae7a6fd971f7447",
"id": "expoAutolinkingConfig:android",
"reasons": [
"expoAutolinkingAndroid",
],
"type": "contents",
},
"op": "added",
},
{
"addedSource": {
"contents": "{"extraDependencies":[],"coreFeatures":[],"modules":[]}",
"debugInfo": {
"hash": "10c4144650e3af1f596683ac4ae7a6fd971f7447",
},
"hash": "10c4144650e3af1f596683ac4ae7a6fd971f7447",
"id": "expoAutolinkingConfig:ios",
"reasons": [
"expoAutolinkingIos",
],
"type": "contents",
},
"op": "added",
},
{
"addedSource": {
"contents": "{"android":{"adaptiveIcon":{"backgroundColor":"#FFFFFF","foregroundImage":"./assets/adaptive-icon.png"}},"assetBundlePatterns":["**/*"],"icon":"./assets/icon.png","ios":{"supportsTablet":true},"name":"sdk47","orientation":"portrait","platforms":["android","ios","web"],"slug":"sdk47","splash":{"backgroundColor":"#ffffff","image":"./assets/splash.png","resizeMode":"contain"},"updates":{"fallbackToCacheTimeout":0},"userInterfaceStyle":"light","version":"1.0.0","web":{"favicon":"./assets/favicon.png"}}",
Expand All @@ -86,13 +116,58 @@ describe(diffFingerprintChangesAsync, () => {
},
"op": "added",
},
{
"addedSource": {
"contents": "{"setup:docs":"./scripts/download-dependencies.sh","setup:native":"./scripts/download-dependencies.sh && ./scripts/setup-react-android.sh","postinstall":"yarn-deduplicate && yarn workspace @expo/cli prepare && patch-package && node ./tools/bin/expotools.js validate-workspace-dependencies","install:react-native-lab":"(([ \\"$(ls -A react-native-lab/react-native)\\" ] && (yarn --cwd react-native-lab/react-native install --frozen-lockfile || true)) || echo \\"Skipping installing Node modules in react-native-lab/react-native (directory empty)\\")","lint":"eslint .","tsc":"echo 'You are trying to run \\"tsc\\" in the workspace root. Run it from an individual package instead.' && exit 1"}",
"debugInfo": {
"hash": "c4f835988805180a373d4ff37cef6ce53cb14995",
},
"hash": "c4f835988805180a373d4ff37cef6ce53cb14995",
"id": "packageJson:scripts",
"reasons": [
"packageJson:scripts",
],
"type": "contents",
},
"op": "added",
},
{
"addedSource": {
"contents": "{}",
"debugInfo": {
"hash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f",
},
"hash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f",
"id": "rncoreAutolinkingConfig:android",
"reasons": [
"rncoreAutolinkingAndroid",
],
"type": "contents",
},
"op": "added",
},
{
"addedSource": {
"contents": "{}",
"debugInfo": {
"hash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f",
},
"hash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f",
"id": "rncoreAutolinkingConfig:ios",
"reasons": [
"rncoreAutolinkingIos",
],
"type": "contents",
},
"op": "added",
},
]
`);
});

it('should return diff from contents changes', async () => {
vol.fromJSON(require('../sourcer/__tests__/fixtures/ExpoManaged47Project.json'));
const packageJson = JSON.parse(vol.readFileSync('/app/package.json', 'utf8').toString());
const packageJson = JSON.parse(vol.readFileSync('/app/package.json', 'utf8'));
jest.doMock('/app/package.json', () => packageJson, { virtual: true });
const fingerprint = await createFingerprintAsync(
'/app',
Expand Down Expand Up @@ -158,7 +233,7 @@ describe(diffFingerprintChangesAsync, () => {
'/app',
await normalizeOptionsAsync('/app', { debug: true })
);
const config = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
const config = JSON.parse(vol.readFileSync('/app/app.json', 'utf8'));
config.expo.jsEngine = 'jsc';
vol.writeFileSync('/app/app.json', JSON.stringify(config, null, 2));
const diff = await diffFingerprintChangesAsync(
Expand Down
14 changes: 7 additions & 7 deletions packages/@expo/fingerprint/src/sourcer/__tests__/Expo-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ describe(getExpoConfigSourcesAsync, () => {

it('should contain expo config', async () => {
vol.fromJSON(require('./fixtures/ExpoManaged47Project.json'));
const appJson = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
const appJson = JSON.parse(vol.readFileSync('/app/app.json', 'utf8'));
const options = await normalizeOptionsAsync('/app');
const { config, loadedModules } = await getExpoConfigAsync('/app', options);
const sources = await getExpoConfigSourcesAsync('/app', config, loadedModules, options);
Expand All @@ -203,7 +203,7 @@ describe(getExpoConfigSourcesAsync, () => {
const { config, loadedModules } = await getExpoConfigAsync('/app', options);
const sources = await getExpoConfigSourcesAsync('/app', config, loadedModules, options);

const appJsonContents = vol.readFileSync('/app/app.json', 'utf8').toString();
const appJsonContents = vol.readFileSync('/app/app.json', 'utf8');
const appJson = JSON.parse(appJsonContents);
const { name } = appJson.expo;
// Re-insert name to change the object order
Expand All @@ -225,7 +225,7 @@ describe(getExpoConfigSourcesAsync, () => {

it('should transform expo config paths as relative paths', async () => {
vol.fromJSON(require('./fixtures/ExpoManaged47Project.json'));
const appJson = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
const appJson = JSON.parse(vol.readFileSync('/app/app.json', 'utf8'));
appJson.expo.extra ||= {};
appJson.expo.extra.testFile = '/app/test-file.txt';
appJson.expo.extra.testNestedFile = '/app/nested/test-file.txt';
Expand Down Expand Up @@ -263,7 +263,7 @@ describe(getExpoConfigSourcesAsync, () => {
vol.writeFileSync('/app/assets/icon-light.png', 'PNG data');
vol.writeFileSync('/app/assets/icon-dark.png', 'PNG data');
vol.writeFileSync('/app/assets/icon-tinted.png', 'PNG data');
const appJson = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
const appJson = JSON.parse(vol.readFileSync('/app/app.json', 'utf8'));
appJson.expo.ios ||= {};
appJson.expo.ios.icon = {
light: '/app/assets/icon-light.png',
Expand Down Expand Up @@ -299,7 +299,7 @@ describe(getExpoConfigSourcesAsync, () => {
vol.fromJSON(require('./fixtures/ExpoManaged47Project.json'));
vol.mkdirSync('/app/assets');
copyDirSync(path.join(__dirname, 'fixtures', 'ExpoGo.icon'), '/app/assets/ExpoGo.icon');
const appJson = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
const appJson = JSON.parse(vol.readFileSync('/app/app.json', 'utf8'));
appJson.expo.ios ||= {};
appJson.expo.ios.icon = '/app/assets/ExpoGo.icon';
vol.writeFileSync('/app/app.json', JSON.stringify(appJson, null, 2));
Expand All @@ -319,7 +319,7 @@ describe(getExpoConfigSourcesAsync, () => {
it('should contain external google service files with override hash key', async () => {
vol.fromJSON(require('./fixtures/ExpoManaged47Project.json'));
vol.writeFileSync('/app/google-services.json', 'JSON data');
const appJson = JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString());
const appJson = JSON.parse(vol.readFileSync('/app/app.json', 'utf8'));
appJson.expo.android ||= {};
appJson.expo.android.googleServicesFile = '/app/google-services.json';
vol.writeFileSync('/app/app.json', JSON.stringify(appJson, null, 2));
Expand All @@ -342,7 +342,7 @@ describe(getExpoConfigSourcesAsync, () => {
vol.writeFileSync('/app/assets/images/splash-icon.png', 'PNG data');

const config = {
exp: JSON.parse(vol.readFileSync('/app/app.json', 'utf8').toString()).expo,
exp: JSON.parse(vol.readFileSync('/app/app.json', 'utf8')).expo,
};
const configResult = JSON.stringify({ config, loadedModules: [] });
const mockSpawnWithIpcAsync = spawnWithIpcAsync as jest.MockedFunction<
Expand Down
2 changes: 1 addition & 1 deletion packages/expo-brownfield/plugin/src/common/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const readTemplate = (template: string, platform?: PlatformString): string => {
throw new Error(`Template ${template} doesn't exist at ${templatePath}`);
}

return readFileSync(templatePath).toString();
return readFileSync(templatePath, 'utf8');
};

const createFileFromTemplateInternal = (
Expand Down
2 changes: 1 addition & 1 deletion packages/pod-install/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async function runAsync(maybeProjectDirectory?: string): Promise<void> {
process.exit(1);
}

const jsonData = JSON.parse(readFileSync(packageJsonPath).toString());
const jsonData = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
const hasExpoPackage = jsonData.dependencies?.hasOwnProperty('expo');

if (hasExpoPackage) {
Expand Down