react-native-esbuild icon indicating copy to clipboard operation
react-native-esbuild copied to clipboard

Supports hot reload

Open leegeunhyeok opened this issue 1 year ago • 7 comments

Description

Supports hot reload on development environment.

Tasks

  • [x] Implement custom file watcher instead esbuild.context.watch()
    • [x] On some files change, trigger rebuild() and get the metadata(path, etc..) of the changed module.
    • [x] Implement custom file system watcher using chokidar
  • [x] Transformer
    • [x] Skip if file is not changed (transform changed file only)
    • [x] Wrap modules with react-refresh context boundary
      • [x] Implement swc plugin (https://github.com/leegeunhyeok/swc-plugin-react-refresh)
    • [x] Custom module system
    • [x] Send HMR update message to client via web socket
    • [x] Evaluate updated code on client runtime
  • [x] Get updated module code from bundle
  • [x] Add experimental config for toggle hot reload

  • [ ] #41

References

  • react-refresh
  • https://swc.rs/docs/configuration/compilation#jsctransformreactrefresh
  • https://github.com/FredKSchott/esm-hmr/blob/master/src/client.ts
  • PoC: https://github.com/leegeunhyeok/esbuild-hmr

Limitations

  • Esbuild isn't supports HMR(Hot Module Replacement).
  • Cannot skip transform(js, ts) loader in Esbuild.

Notes

  • 0.1.0-alpha.42

// before
var __commonJS = (cb, mod) => function __require2() {
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};

// after
global.cachedModule = {};
var __commonJS = (cb, mod) => {
  var name = __getOwnPropNames(cb)[0];
  Object.defineProperty(global.cachedModule, name, {
    get: () => mod
  });
  return function __require2() {
    return mod || (0, cb[name])((mod = { exports: {} }).exports, mod), mod.exports;
  };
};

// when file changes detected -> don't rebuild, just transform target file with swc only.
// `import { a, b, c } from 'module-path';`

try {
  const { a, b, c } = global.cachedModule['module-path'];

  // ...
} catch (error) {
  // hot reload failed. fully reload instead
}
// Esbuild metafile
interface Metafile {
  // `inputs` key is module file path.
  inputs: Record<string, {
    byte: number;
    imports: {
      path: string; // eg. 'node_modules/react/cjs/index.js'
      original: string; // eg. 'react'
      kind: '...',
    }[];
  }>[],
  // ...
}

// {
//   react: 'node_modules/react/cjs/index.js',
//   '../components': 'src/components/index.ts',
// }
const mappedModules = metafile.inputs[changedFilePath]?.imports.reduce((prev, curr) => {
  return { ...prev, [curr.original]: curr.path };
}, {});

// Example usage
import { transform } from '@swc/core';

await transform(code, {
  jsc: {
    experimental: {
      plugins: [
        // https://github.com/leegeunhyeok/swc-plugin-react-native-esbuild-module
        ['react-native-esbuild-module-plugin', {
          alias: mappedModules ?? {},
        }],
      ],
    },
  },
});

leegeunhyeok avatar Oct 11 '23 05:10 leegeunhyeok