{"record":{"id":"cbe9fd4680fd1318","repo":"facebook/react","slug":"expected-resolve-to-have-been-called-before-transf","errorCode":null,"errorMessage":"Expected resolve to have been called before transformSource","messagePattern":"Expected resolve to have been called before transformSource","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js","lineNumber":502,"sourceCode":"      return;\n    case 'ParenthesizedExpression':\n      addExportNames(names, node.expression);\n      return;\n  }\n}\n\nfunction resolveClientImport(\n  specifier: string,\n  parentURL: string,\n): {url: string} | Promise<{url: string}> {\n  // Resolve an import specifier as if it was loaded by the client. This doesn't use\n  // the overrides that this loader does but instead reverts to the default.\n  // This resolution algorithm will not necessarily have the same configuration\n  // as the actual client loader. It should mostly work and if it doesn't you can\n  // always convert to explicit exported names instead.\n  const conditions = ['node', 'import'];\n  if (stashedResolve === null) {\n    throw new Error(\n      'Expected resolve to have been called before transformSource',\n    );\n  }\n  return stashedResolve(specifier, {conditions, parentURL}, stashedResolve);\n}\n\nasync function parseExportNamesInto(\n  body: any,\n  names: Array<string>,\n  parentURL: string,\n  loader: LoadFunction,\n): Promise<void> {\n  for (let i = 0; i < body.length; i++) {\n    const node = body[i];\n    switch (node.type) {\n      case 'ExportAllDeclaration':\n        if (node.exported) {\n          addExportNames(names, node.exported);","sourceCodeStart":484,"sourceCodeEnd":520,"githubUrl":"https://github.com/facebook/react/blob/eafeac097ba51e1eab809c07102126bd5f8e5425/packages/react-server-dom-esm/src/ReactFlightESMNodeLoader.js#L484-L520","documentation":"The RSC ESM loader implements Node's ESM hooks: during its resolve hook it stashes the next resolve hook from the chain (stashedResolve). transformSource later needs it inside resolveClientImport() to re-resolve `export * from` specifiers the way a client would. If transformSource runs before this loader's resolve hook ever ran (stashedResolve === null), the loader's ordering invariant was broken and it throws.","triggerScenarios":"A hand-built or mis-chained loader setup where a preceding resolve hook short-circuits without calling nextResolve, so this loader's resolve never runs; invoking the loader's transformSource export directly instead of going through Node's module system; module runners or test harnesses that call transform hooks without resolve; Node loader-API version drift changing hook invocation order.","commonSituations":"Combining react-server-dom-esm's loader with other --experimental-loader entries (tsx, ts-node) that swallow the chain; upgrading Node and re-wiring module.register() incorrectly; copying the loader into a custom server harness that bypasses resolve.","solutions":["Register the full loader module exactly as documented (node --experimental-loader or module.register on the complete loader), so both its resolve and transformSource hooks engage","Ensure any custom resolve hooks in the chain forward to nextResolve instead of returning early","Never call the loader's transformSource export manually; drive everything through Node's module system","Check the react-server-dom-esm version against your Node version — the loader hook contract changed across Node 18/20/22"],"exampleFix":"// before (custom harness calling the hook directly)\nimport {transformSource} from 'react-server-dom-esm/npm/loader.js';\nconst r = await transformSource(url, source, format); // throws\n\n// after (register the whole loader)\nimport {register} from 'node:module';\nregister('react-server-dom-esm/npm/loader.js', import.meta.url);","handlingStrategy":"validation","validationCode":"// correct registration ensures resolve runs before transformSource\n// server-entry.mjs\nimport {register} from 'node:module';\nregister('react-server-dom-esm/npm/loader.js', import.meta.url);\n// then start: node --conditions react-server server-entry.mjs","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Register the complete loader module via module.register or --experimental-loader; never call its hooks directly","Make custom resolve hooks in the chain always forward via nextResolve","Pin the react-server-dom-esm version to one tested against your Node release"],"tags":["node-loader","esm","react-server-components","loader-chaining"],"backgroundTag":"node-loader-hook-misregistration","analyzedSha":"eafeac097ba51e1eab809c07102126bd5f8e5425","analyzedAt":"2026-08-21T22:01:08.818Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}