brk_rolldown_plugin_hmr 0.8.0

Rolldown plugin for Hot Module Replacement (HMR)
Documentation
// @ts-check

/** @import { DevRuntime, Messenger, DevRuntimeMessage } from './runtime-extra-dev-common.js' */

/** @type {typeof DevRuntime} */
// @ts-expect-error -- there's no way to declare a variable by JSDoc
var BaseDevRuntime = DevRuntime;

class ModuleHotContext {
  /**
   * @type {{ deps: [string], fn: (moduleExports: Record<string, any>[]) => void }[]}
   */
  acceptCallbacks = [];
  /**
   * @param {string} moduleId
   * @param {InstanceType<BaseDevRuntime>} devRuntime
   */
  constructor(moduleId, devRuntime) {
    this.moduleId = moduleId;
    this.devRuntime = devRuntime;
  }

  /**
   * @overload
   * @param {(mod: Record<string, any>) => void} cb
   * @returns {void}
   */
  /**
   * @param {...any} args
   * @returns {void}
   */
  accept(...args) {
    if (args.length === 1) {
      const [cb] = /** @type {[(mod: Record<string, any>) => void]} */ (args);
      const acceptingPath = this.moduleId;
      this.acceptCallbacks.push({
        deps: [acceptingPath],
        fn: cb,
      });
    } else if (args.length === 0) {}
    else {
      throw new Error('Invalid arguments for `import.meta.hot.accept`');
    }
  }

  invalidate() {
    socket.send(JSON.stringify({
      type: 'hmr:invalidate',
      moduleId: this.moduleId,
    }));
  }
}

class DefaultDevRuntime extends BaseDevRuntime {
  /**
   * @param {WebSocket} socket
   * @param {string} clientId
   */
  constructor(socket, clientId) {
    /** @type {string[]} */
    const queuedMessages = [];
    /** @type {Messenger} */
    const messenger = {
      send(message) {
        if (socket.readyState === WebSocket.OPEN) {
          socket.send(JSON.stringify(message));
        } else if (socket.readyState === WebSocket.CLOSED) {
          // Do nothing
        } else {
          queuedMessages.push(JSON.stringify(message));
        }
      },
    };
    socket.onopen = () => {
      for (const message of queuedMessages) {
        socket.send(message);
      }
      socket.onopen = null;
    };

    super(messenger, clientId);
  }

  /**
   * @type {Map<string, ModuleHotContext>}
   */
  moduleHotContexts = new Map();
  /**
   * @type {Map<string, ModuleHotContext>}
   */
  moduleHotContextsToBeUpdated = new Map();
  /**
   * @override
   * @param {string} moduleId
   */
  createModuleHotContext(moduleId) {
    const hotContext = new ModuleHotContext(moduleId, this);
    if (this.moduleHotContexts.has(moduleId)) {
      this.moduleHotContextsToBeUpdated.set(moduleId, hotContext);
    } else {
      this.moduleHotContexts.set(moduleId, hotContext);
    }
    return hotContext;
  }
  /**
   * @override
   * @param {[string, string][]} boundaries
   */
  applyUpdates(boundaries) {
    // trigger callbacks of accept() correctly
    for (let [moduleId, acceptedVia] of boundaries) {
      const hotContext = this.moduleHotContexts.get(moduleId);
      if (hotContext) {
        const acceptCallbacks = hotContext.acceptCallbacks;
        acceptCallbacks.filter((cb) => {
          cb.fn(this.modules[moduleId].exports);
        });
      }
    }
    this.moduleHotContextsToBeUpdated.forEach((hotContext, moduleId) => {
      this.moduleHotContexts.set(moduleId, hotContext);
    });
    this.moduleHotContextsToBeUpdated.clear();
    // swap new contexts
  }
}

/** @param {string} url */
function loadScript(url) {
  var script = document.createElement('script');
  script.src = url;
  script.type = 'module';
  script.onerror = function() {
    console.error('Failed to load script: ' + url);
  };
  document.body.appendChild(script);
}

console.debug('HMR runtime loaded', '$ADDR');
// Generate client ID immediately at runtime initialization
// This ensures the client ID is available before any lazy imports
const clientId = crypto.randomUUID();
const addr = new URL('ws://$ADDR');
addr.searchParams.set('clientId', clientId);

const socket = new WebSocket(addr);

(/** @type {any} */ (globalThis)).__rolldown_runtime__ ??=
  new DefaultDevRuntime(socket, clientId);

/** @param {MessageEvent} event */
socket.onmessage = function(event) {
  const data = JSON.parse(event.data);
  console.debug('Received message:', data);
  if (data.type === 'connected') {
    // Server acknowledged the connection
    console.debug('[hmr]: Connection established with server');
  } else if (data.type === 'hmr:update') {
    if (typeof process === 'object') {
      import(data.path);
      console.debug(`[hmr]: Importing HMR patch: ${data.path}`);
    } else {
      console.debug(`[hmr]: Loading HMR patch: ${data.path}`);
      loadScript(data.url);
    }
  } else if (data.type === 'hmr:reload') {
    console.log('[hmr]: Full reload required, reloading page');
    if (typeof location !== 'undefined') {
      location.reload();
    } else {
      console.log('[hmr]: location is undefined, cannot reload page');
    }
  }
};