Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
leegeunhyeok committed Nov 27, 2023
1 parent c250fad commit cb9a38d
Show file tree
Hide file tree
Showing 13 changed files with 273 additions and 88 deletions.
77 changes: 56 additions & 21 deletions packages/core/lib/bundler/bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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 +28,6 @@ import type {
BundleResult,
BundleRequestOptions,
PluginContext,
UpdatedModule,
ReportableEvent,
ReactNativeEsbuildPluginCreator,
} from '../types';
Expand All @@ -42,13 +42,14 @@ import {
getResolveExtensionsOption,
getLoaderOption,
getEsbuildWebConfig,
getHmrUpdatedModule,
getHmrRuntimeTransformer,
} 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>[] = [];
Expand Down Expand Up @@ -228,8 +229,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 +261,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 +273,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,12 +305,6 @@ 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,
);

currentTask.handler?.resolver?.({
result: {
source: bundleOutput.contents,
Expand All @@ -319,21 +318,41 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
currentTask.handler?.rejecter?.(error);
} finally {
currentTask.status = 'resolved';
this.emit('build-end', {
revisionId,
id: context.id,
additionalData: context.additionalData,
updatedModule,
});
Promise.resolve(
sharedStorage.hmr.path
? hmrController.getUpdates(
sharedStorage.hmr.path,
data.result.metafile,
)
: null,
)
.then((update) => {
this.emit('build-end', {
revisionId,
id: context.id,
additionalData: context.additionalData,
update,
});
})
.catch((error) => {
logger.error('unexpected error', error as Error);
this.emit('build-end', {
revisionId,
id: context.id,
additionalData: context.additionalData,
update: null,
});
});
}
}

private async getOrCreateBundleTask(
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 +373,22 @@ export class ReactNativeEsbuildBundler extends BundlerEventEmitter {
logger.debug(`bundle task is now watching (id: ${targetTaskId})`);
}

// HMR Controller
if (!ReactNativeEsbuildBundler.hmr.has(targetTaskId)) {
ReactNativeEsbuildBundler.hmr.set(
targetTaskId,
new HmrTransformer(
this.root,
getHmrRuntimeTransformer(
targetTaskId,
this.root,
this.config,
bundleOptions,
),
),
);
}

// 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 +475,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 +491,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
65 changes: 44 additions & 21 deletions packages/core/lib/bundler/helpers/hmr.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,48 @@
import {
getModuleCodeFromBundle,
isReactRefreshRegistered,
} from '@react-native-esbuild/hmr';
import type { UpdatedModule } from '../../types';
AsyncTransformPipeline,
swcPresets,
} from '@react-native-esbuild/transformer';
import type { BundleOptions } from '@react-native-esbuild/config';
import type { Config } from '../../types';

export const getHmrUpdatedModule = (
id: string | null,
path: string | null,
bundleCode: string,
): UpdatedModule | null => {
const updatedCode =
id && path ? getModuleCodeFromBundle(bundleCode, id) : null;
const DUMMY_ESBUILD_ARGS = {
namespace: '',
suffix: '',
pluginData: undefined,
} as const;

return updatedCode
? {
code: updatedCode,
id: id ?? '',
path: path ?? '',
mode: isReactRefreshRegistered(updatedCode)
? 'hot-reload'
: 'full-reload',
}
: null;
export const getHmrRuntimeTransformer = (
id: number,
root: string,
config: Config,
bundleOptions: BundleOptions,
): ((code: string, path: string) => Promise<string>) => {
const {
stripFlowPackageNames = [],
fullyTransformPackageNames = [],
additionalTransformRules,
} = config.transformer ?? {};
const additionalBabelRules = additionalTransformRules?.babel ?? [];
const additionalSwcRules = additionalTransformRules?.swc ?? [];

const transformPipeline = new AsyncTransformPipeline.builder({
...bundleOptions,
id,
root,
})
.setSwcPreset(swcPresets.getReactNativeRuntimePreset())
.setFullyTransformPackages(fullyTransformPackageNames)
.setStripFlowPackages(stripFlowPackageNames)
.setAdditionalBabelTransformRules(additionalBabelRules)
.setAdditionalSwcTransformRules(additionalSwcRules)
.build();

return (code: string, path: string) => {
return transformPipeline
.transform(code, {
...DUMMY_ESBUILD_ARGS,
path,
})
.then((result) => result.code);
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,16 @@ export const createMetafilePlugin: ReactNativeEsbuildPluginCreator = (
name: NAME,
setup: (build): void => {
build.onEnd(async (result: BuildResult) => {
const { metafile } = result;
if (!(context.metafile && result.metafile)) return;

const filename = path.join(
context.root,
`metafile-${context.platform}-${new Date().getTime().toString()}.json`,
);

if (metafile) {
logger.debug('writing esbuild metafile', { destination: filename });
await fs.writeFile(filename, JSON.stringify(metafile), {
encoding: 'utf-8',
});
}
logger.debug('writing esbuild metafile', { destination: filename });
await fs.writeFile(filename, JSON.stringify(result.metafile), {
encoding: 'utf-8',
});
});
},
});
6 changes: 0 additions & 6 deletions packages/core/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,6 @@ export interface BundlerSharedData {
}

export type BundlerAdditionalData = Record<string, unknown>;
export interface UpdatedModule {
id: string;
path: string;
code: string;
mode: 'hot-reload' | 'full-reload';
}

export interface PluginContext extends BundleOptions {
id: number;
Expand Down
21 changes: 9 additions & 12 deletions packages/dev-server/lib/server/ReactNativeAppServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,18 +147,15 @@ export class ReactNativeAppServer extends DevServer {
};

this.bundler.on('build-start', hmr.updateStart);
this.bundler.on(
'build-end',
({ revisionId, updatedModule, additionalData }) => {
// `additionalData` can be `{ disableRefresh: true }` by `serve-asset-middleware`.
if (!additionalData?.disableRefresh) {
updatedModule?.mode === 'hot-reload'
? hmr.hotReload(revisionId, updatedModule.code)
: hmr.liveReload(revisionId);
}
hmr.updateDone();
},
);
this.bundler.on('build-end', ({ revisionId, update, additionalData }) => {
// `additionalData` can be `{ disableRefresh: true }` by `serve-asset-middleware`.
if (!additionalData?.disableRefresh) {
update?.fullyReload
? hmr.liveReload(revisionId)
: hmr.hotReload(revisionId, update.code);
}
hmr.updateDone();
});

this.server.on('upgrade', (request, socket, head) => {
if (!request.url) return;
Expand Down
17 changes: 7 additions & 10 deletions packages/dev-server/lib/server/ReactNativeWebServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,16 +102,13 @@ export class ReactNativeWebServer extends DevServer {

const { server: hmrServer, ...hmr } = createHmrMiddlewareForWeb();

this.bundler.on(
'build-end',
({ revisionId, updatedModule, additionalData }) => {
if (!additionalData?.disableRefresh) {
updatedModule?.mode === 'hot-reload'
? hmr.hotReload(revisionId, updatedModule.code)
: hmr.liveReload(revisionId);
}
},
);
this.bundler.on('build-end', ({ revisionId, update, additionalData }) => {
if (!additionalData?.disableRefresh) {
update?.fullyReload
? hmr.liveReload(revisionId)
: hmr.hotReload(revisionId, update.code);
}
});

const symbolicateMiddleware = createSymbolicateMiddleware(
{ bundler, devServerOptions: this.devServerOptions },
Expand Down
Loading

0 comments on commit cb9a38d

Please sign in to comment.