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: 2 additions & 0 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import './src/utils/assetInlinePolyfill';
import { StatusBar } from 'expo-status-bar';
import React, { useEffect, useRef } from 'react';
import { Alert, AppState, AppStateStatus, LogBox } from 'react-native';


import StorybookUI from './.rnstorybook';
import './global.css';

Expand Down
Binary file added assets/images/test-tiny-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module.exports = {
},
transformIgnorePatterns: [
// Transform all expo-* packages and other native modules
'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(-.*)?|@expo(-.*)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg)',
'node_modules/(?!(.pnpm/.*?/node_modules/)?((jest-)?react-native|@react-native(-community)?|expo(-.*)?|@expo(-.*)?|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@sentry/react-native|native-base|react-native-svg))',
],
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts', '!src/**/index.ts'],
testPathIgnorePatterns: ['/node_modules/'],
Expand Down
8 changes: 8 additions & 0 deletions metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,12 @@ nativewindConfig.serializer.customSerializer = wrapWithRouteSizeAnalyzer(
nativewindConfig.serializer.customSerializer,
);

// Register Metro asset inlining plugin for Issue #369
nativewindConfig.transformer ??= {};
nativewindConfig.transformer.assetPlugins = [
...(nativewindConfig.transformer.assetPlugins || []),
require.resolve('./tools/metro-plugins/imageInlinePlugin.js'),
];

module.exports = nativewindConfig;

15 changes: 15 additions & 0 deletions src/utils/assetInlinePolyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import AssetSourceResolver from 'react-native/Libraries/Image/AssetSourceResolver';

const originalDefaultAsset = AssetSourceResolver.prototype.defaultAsset;

AssetSourceResolver.prototype.defaultAsset = function () {
try {
if (this.asset && typeof this.asset.base64 === 'string') {
return this.fromSource(this.asset.base64);
}
} catch (error) {
// Non-blocking warning: fall back to normal asset resolution
console.warn('[asset-inline-polyfill] Error resolving inline asset:', error);
}
return originalDefaultAsset.call(this);
};
51 changes: 51 additions & 0 deletions tests/utils/assetInlining.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, it, expect } from '@jest/globals';
import AssetSourceResolver from 'react-native/Libraries/Image/AssetSourceResolver';
import '../../src/utils/assetInlinePolyfill'; // Import to apply the polyfill

describe('Asset Inlining Polyfill', () => {
it('should resolve an inlined asset to its base64 data URL', () => {
const mockInlinedAsset = {
__packager_asset: true,
fileSystemLocation: '/path/to/assets',
httpServerLocation: '/assets',
width: 48,
height: 48,
scales: [1],
hash: '4f1cb2cac2370cd5050681232e8575a8',
name: 'test-tiny-icon',
type: 'png',
base64: 'data:image/png;base64,iVBORw0KGgoAAAANS...', // custom inlined data URL
} as any;

const resolver = new AssetSourceResolver('http://localhost:8081', null, mockInlinedAsset);
const resolved = resolver.defaultAsset();

expect(resolved).toBeDefined();
expect(resolved.uri).toBe(mockInlinedAsset.base64);
expect(resolved.width).toBe(48);
expect(resolved.height).toBe(48);
});

it('should fall back to standard resolution for non-inlined assets', () => {
const mockStandardAsset = {
__packager_asset: true,
fileSystemLocation: '/path/to/assets',
httpServerLocation: '/assets',
width: 48,
height: 48,
scales: [1],
hash: '4f1cb2cac2370cd5050681232e8575a8',
name: 'react-logo',
type: 'png',
// no base64 key
} as any;

const resolver = new AssetSourceResolver('http://localhost:8081', null, mockStandardAsset);
const resolved = resolver.defaultAsset();

expect(resolved).toBeDefined();
// Default URL-based asset resolution
expect(resolved.uri).toContain('react-logo.png');
expect(resolved.uri.startsWith('data:')).toBe(false);
});
});
30 changes: 30 additions & 0 deletions tools/metro-plugins/imageInlinePlugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const fs = require('fs');

const INLINE_LIMIT = 1024; // 1 KB
const SUPPORTED_TYPES = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp']);

module.exports = function imageInlinePlugin(assetData) {
const type = assetData.type.toLowerCase();

if (SUPPORTED_TYPES.has(type)) {
const primaryFile = assetData.files[0];
if (primaryFile && fs.existsSync(primaryFile)) {
try {
const stat = fs.statSync(primaryFile);
if (stat.size < INLINE_LIMIT) {
const mime = type === 'jpg' ? 'jpeg' : type;
const base64 = fs.readFileSync(primaryFile, 'base64');
assetData.base64 = `data:image/${mime};base64,${base64}`;
}
} catch (error) {
// Non-fatal build warning: must not break compilation
console.warn(
`[image-inline-plugin] Failed to evaluate or inline asset ${primaryFile}:`,
error.message
);
}
}
}

return assetData;
};
Loading