Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
leegeunhyeok committed Dec 14, 2023
1 parent 7474af5 commit 85936fd
Show file tree
Hide file tree
Showing 42 changed files with 927 additions and 416 deletions.
Binary file not shown.
4 changes: 2 additions & 2 deletions example/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AppRegistry } from 'react-native';
import { App } from './src/App';
import { name as appName } from './app.json';
// import { name as appName } from './app.json';

AppRegistry.registerComponent(appName, () => App);
AppRegistry.registerComponent('example', () => App);
1 change: 1 addition & 0 deletions example/metafile-ios-1701305959515.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion example/src/screens/IntroScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function IntroScreen(): React.ReactElement {
<Button label="GitHub" onPress={handlePressGitHub} />
</Section>
<Section title="Experimental">
<Description>This project is under development.</Description>
<Description>This project is under development</Description>
<Text variant="danger">CHECK & TEST BEFORE USING IN PRODUCTION</Text>
</Section>
<View sx={{ marginBottom: '$04' }} />
Expand Down
113 changes: 81 additions & 32 deletions packages/core/lib/bundler/bundler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import path from 'node:path';
import type { Stats } from 'node:fs';
import esbuild, {
type BuildOptions,
type BuildResult,
Expand All @@ -7,6 +8,7 @@ import esbuild, {
import invariant from 'invariant';
import ora from 'ora';
import { getGlobalVariables } from '@react-native-esbuild/internal';
import { HmrTransformer } from '@react-native-esbuild/hmr';
import {
setEnvironment,
combineWithDefaultBundleOptions,
Expand All @@ -27,7 +29,6 @@ import type {
BundleResult,
BundleRequestOptions,
PluginContext,
UpdatedModule,
ReportableEvent,
ReactNativeEsbuildPluginCreator,
} from '../types';
Expand All @@ -42,18 +43,22 @@ import {
getResolveExtensionsOption,
getLoaderOption,
getEsbuildWebConfig,
getHmrUpdatedModule,
getExternalFromPackageJson,
getExternalModulePattern,
} from './helpers';
import { printLogo, printVersion } from './logo';

export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
public static caches = CacheStorage.getInstance();
public static shared = SharedStorage.getInstance();
private static hmr = new Map<number, HmrTransformer>();
private appLogger = new Logger('app', LogLevel.Trace);
private buildTasks = new Map<number, BuildTask>();
private plugins: ReactNativeEsbuildPluginCreator<unknown>[] = [];
private initialized = false;
private config: Config;
private external: string[];
private externalPattern: string;
private initialized = false;

/**
* Must be bootstrapped first at the entry point
Expand Down Expand Up @@ -100,6 +105,11 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
constructor(private root: string = process.cwd()) {
super();
this.config = getConfigFromGlobal();
this.external = getExternalFromPackageJson(root);
this.externalPattern = getExternalModulePattern(
this.external,
this.config.resolver?.assetExtensions ?? [],
);
this.on('report', (event) => {
this.broadcastToReporter(event);
});
Expand Down Expand Up @@ -133,20 +143,7 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {

private startWatcher(): Promise<void> {
return FileSystemWatcher.getInstance()
.setHandler((event, changedFile, stats) => {
const hasTask = this.buildTasks.size > 0;
const isChanged = event === 'change';
ReactNativeEsbuildBundler.shared.setValue({
watcher: {
changed: hasTask && isChanged ? changedFile : null,
stats: stats ?? null,
},
});

for (const { context, handler } of this.buildTasks.values()) {
context.rebuild().catch((error) => handler?.rejecter?.(error));
}
})
.setHandler(this.handleFileChanged.bind(this))
.watch(this.root);
}

Expand All @@ -160,7 +157,7 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
invariant(config.resolver, 'invalid resolver configuration');
invariant(config.resolver.mainFields, 'invalid mainFields');
invariant(config.transformer, 'invalid transformer configuration');
invariant(config.resolver.assetExtensions, 'invalid assetExtension');
invariant(config.resolver.assetExtensions, 'invalid assetExtensions');
invariant(config.resolver.sourceExtensions, 'invalid sourceExtensions');
setEnvironment(bundleOptions.dev);

Expand All @@ -179,14 +176,15 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
id: this.identifyTaskByBundleOptions(bundleOptions),
root: this.root,
config: this.config,
externalPattern: this.externalPattern,
mode,
additionalData,
};

return {
entryPoints: [bundleOptions.entry],
outfile: bundleOptions.outfile,
sourceRoot: path.dirname(bundleOptions.entry),
sourceRoot: this.root,
mainFields: config.resolver.mainFields,
resolveExtensions: getResolveExtensionsOption(
bundleOptions,
Expand Down Expand Up @@ -228,8 +226,8 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
logLevel: 'silent',
bundle: true,
sourcemap: true,
metafile: true,
minify: bundleOptions.minify,
metafile: bundleOptions.metafile,
write: mode === 'bundle',
...webSpecifiedOptions,
};
Expand Down Expand Up @@ -260,6 +258,8 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
data: { result: BuildResult; success: boolean },
context: PluginContext,
): void {
invariant(data.result.metafile, 'invalid metafile');

/**
* Exit at the end of a build in bundle mode.
*
Expand All @@ -270,17 +270,19 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
process.exit(1);
}

const hmrSharedValue = ReactNativeEsbuildBundler.shared.get(context.id);
const hmrController = ReactNativeEsbuildBundler.hmr.get(context.id);
const sharedStorage = ReactNativeEsbuildBundler.shared.get(context.id);
const currentTask = this.buildTasks.get(context.id);
invariant(hmrSharedValue, 'invalid hmr shared value');

invariant(sharedStorage, 'invalid shared storage');
invariant(hmrController, 'no hmr controller');
invariant(currentTask, 'no task');

const bundleEndedAt = new Date();
const bundleFilename = context.outfile;
const bundleSourcemapFilename = `${bundleFilename}.map`;
const revisionId = bundleEndedAt.getTime().toString();
const { outputFiles } = data.result;
let updatedModule: UpdatedModule | null = null;

const findFromOutputFile = (
filename: string,
Expand All @@ -300,11 +302,8 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
invariant(bundleOutput, 'empty bundle output');
invariant(bundleSourcemapOutput, 'empty sourcemap output');

updatedModule = getHmrUpdatedModule(
hmrSharedValue.hmr.id,
hmrSharedValue.hmr.path,
bundleOutput.text,
);
const bundleMeta = HmrTransformer.createBundleMeta(data.result.metafile);
ReactNativeEsbuildBundler.shared.setValue({ bundleMeta });

currentTask.handler?.resolver?.({
result: {
Expand All @@ -323,17 +322,41 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
revisionId,
id: context.id,
additionalData: context.additionalData,
updatedModule,
update: null,
});
}
}

private async getOrCreateBundleTask(
private handleFileChanged(
event: string,
changedFile: string,
_stats?: Stats,
): void {
const hasTask = this.buildTasks.size > 0;
const isChanged = event === 'change';
if (!(hasTask && isChanged)) return;

for (const [id, hmrController] of ReactNativeEsbuildBundler.hmr.entries()) {
const { bundleMeta } = ReactNativeEsbuildBundler.shared.get(id);
Promise.resolve(
bundleMeta ? hmrController.getDelta(changedFile, bundleMeta) : null,
).then((update) => {
this.emit('build-end', {
id,
update,
revisionId: new Date().getTime().toString(),
});
});
}
}

private async getOrSetupTask(
bundleOptions: BundleOptions,
additionalData?: BundlerAdditionalData,
): Promise<BuildTask> {
const targetTaskId = this.identifyTaskByBundleOptions(bundleOptions);

// Build Task
if (!this.buildTasks.has(targetTaskId)) {
logger.debug(`bundle task not registered (id: ${targetTaskId})`);
const buildOptions = await this.getBuildOptionsForBundler(
Expand All @@ -354,6 +377,32 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
logger.debug(`bundle task is now watching (id: ${targetTaskId})`);
}

// HMR Transformer
if (!ReactNativeEsbuildBundler.hmr.has(targetTaskId)) {
const {
stripFlowPackageNames,
fullyTransformPackageNames,
additionalTransformRules,
} = this.config.transformer ?? {};
ReactNativeEsbuildBundler.hmr.set(
targetTaskId,
new HmrTransformer(
{
...bundleOptions,
id: targetTaskId,
root: this.root,
externalPattern: this.externalPattern,
},
{
additionalBabelRules: additionalTransformRules?.babel,
additionalSwcRules: additionalTransformRules?.swc,
fullyTransformPackageNames,
stripFlowPackageNames,
},
),
);
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- Already `set()` if not exist.
return this.buildTasks.get(targetTaskId)!;
}
Expand Down Expand Up @@ -440,7 +489,7 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
throw new Error('serve mode is only available on web platform');
}

const buildTask = await this.getOrCreateBundleTask(
const buildTask = await this.getOrSetupTask(
combineWithDefaultBundleOptions(bundleOptions),
additionalData,
);
Expand All @@ -456,7 +505,7 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
additionalData?: BundlerAdditionalData,
): Promise<BundleResult> {
this.throwIfNotInitialized();
const buildTask = await this.getOrCreateBundleTask(
const buildTask = await this.getOrSetupTask(
combineWithDefaultBundleOptions(bundleOptions),
additionalData,
);
Expand Down
4 changes: 2 additions & 2 deletions packages/core/lib/bundler/events/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import EventEmitter from 'node:events';
import type { BundleUpdate } from '@react-native-esbuild/hmr';
import type {
BundlerAdditionalData,
BuildStatus,
ReportableEvent,
UpdatedModule,
} from '../../types';

export class BundlerEventEmitter extends EventEmitter {
Expand Down Expand Up @@ -47,7 +47,7 @@ export interface BundlerEventPayload {
'build-end': {
id: number;
revisionId: string;
updatedModule: UpdatedModule | null;
update: BundleUpdate | null;
additionalData?: BundlerAdditionalData;
};
'build-status-change': BuildStatus & {
Expand Down
7 changes: 0 additions & 7 deletions packages/core/lib/bundler/helpers/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,6 @@ export const loadConfig = (configFilePath?: string): Config => {
assetExtensions: ASSET_EXTENSIONS,
},
transformer: {
jsc: {
transform: {
react: {
runtime: 'automatic',
},
},
},
stripFlowPackageNames: ['react-native'],
},
web: {
Expand Down
14 changes: 14 additions & 0 deletions packages/core/lib/bundler/helpers/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import fs from 'node:fs';
import path from 'node:path';

export const getExternalFromPackageJson = (root: string): string[] => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- allow any.
const { dependencies = {} } = JSON.parse(
fs.readFileSync(path.join(root, 'package.json'), 'utf-8'),
);
return [
'react/jsx-runtime',
'@react-navigation/devtools',
...Object.keys(dependencies as Record<string, string>),
];
};
25 changes: 0 additions & 25 deletions packages/core/lib/bundler/helpers/hmr.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/core/lib/bundler/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './async';
export * from './config';
export * from './hmr';
export * from './fs';
export * from './internal';
Loading

1 comment on commit 85936fd

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

❌ An unexpected error occurred. For more details, check console

Error: The process '/opt/hostedtoolcache/node/18.19.0/x64/bin/npx' failed with exit code 1
St.
Category Percentage Covered / Total
🔴 Statements 14.84% 328/2210
🔴 Branches 14.21% 82/577
🔴 Functions 9.85% 65/660
🔴 Lines 14.18% 298/2101

Test suite run failed

Failed tests: 1/83. Failed suites: 1/10.
  ● getAssetRegistrationScript › should match snapshot

    expect(received).toMatchSnapshot()

    Snapshot name: `getAssetRegistrationScript should match snapshot 1`

    - Snapshot  - 3
    + Received  + 1

    - "
    -     module.exports = require('react-native/Libraries/Image/AssetRegistry').registerAsset({"__packager_asset":true,"name":"image","type":"png","scales":[1,2,3],"hash":"hash","httpServerLocation":"/image.png","width":0,"height":0});
    -   "
    + "module.exports =require('react-native/Libraries/Image/AssetRegistry').registerAsset({"__packager_asset":true,"name":"image","type":"png","scales":[1,2,3],"hash":"hash","httpServerLocation":"/image.png","width":0,"height":0});"

      19 |
      20 |   it('should match snapshot', () => {
    > 21 |     expect(assetRegistrationScript).toMatchSnapshot();
         |                                     ^
      22 |   });
      23 | });
      24 |

      at Object.toMatchSnapshot (packages/internal/lib/__tests__/presets.test.ts:21:37)

Report generated by 🧪jest coverage report action from 85936fd

Please sign in to comment.