wrappingLoader.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. var {
  2. _optionalChain
  3. } = require('@sentry/utils');
  4. Object.defineProperty(exports, '__esModule', { value: true });
  5. const fs = require('fs');
  6. const path = require('path');
  7. const commonjs = require('@rollup/plugin-commonjs');
  8. const utils = require('@sentry/utils');
  9. const chalk = require('chalk');
  10. const rollup = require('rollup');
  11. // Just a simple placeholder to make referencing module consistent
  12. const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module';
  13. // Needs to end in .cjs in order for the `commonjs` plugin to pick it up
  14. const WRAPPING_TARGET_MODULE_NAME = '__SENTRY_WRAPPING_TARGET_FILE__.cjs';
  15. const apiWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'apiWrapperTemplate.js');
  16. const apiWrapperTemplateCode = fs.readFileSync(apiWrapperTemplatePath, { encoding: 'utf8' });
  17. const pageWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'pageWrapperTemplate.js');
  18. const pageWrapperTemplateCode = fs.readFileSync(pageWrapperTemplatePath, { encoding: 'utf8' });
  19. const middlewareWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'middlewareWrapperTemplate.js');
  20. const middlewareWrapperTemplateCode = fs.readFileSync(middlewareWrapperTemplatePath, { encoding: 'utf8' });
  21. let showedMissingAsyncStorageModuleWarning = false;
  22. const sentryInitWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'sentryInitWrapperTemplate.js');
  23. const sentryInitWrapperTemplateCode = fs.readFileSync(sentryInitWrapperTemplatePath, { encoding: 'utf8' });
  24. const serverComponentWrapperTemplatePath = path.resolve(
  25. __dirname,
  26. '..',
  27. 'templates',
  28. 'serverComponentWrapperTemplate.js',
  29. );
  30. const serverComponentWrapperTemplateCode = fs.readFileSync(serverComponentWrapperTemplatePath, { encoding: 'utf8' });
  31. const routeHandlerWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'routeHandlerWrapperTemplate.js');
  32. const routeHandlerWrapperTemplateCode = fs.readFileSync(routeHandlerWrapperTemplatePath, { encoding: 'utf8' });
  33. /**
  34. * Replace the loaded file with a wrapped version the original file. In the wrapped version, the original file is loaded,
  35. * any data-fetching functions (`getInitialProps`, `getStaticProps`, and `getServerSideProps`) or API routes it contains
  36. * are wrapped, and then everything is re-exported.
  37. */
  38. // eslint-disable-next-line complexity
  39. function wrappingLoader(
  40. userCode,
  41. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  42. userModuleSourceMap,
  43. ) {
  44. // We know one or the other will be defined, depending on the version of webpack being used
  45. const {
  46. pagesDir,
  47. appDir,
  48. pageExtensionRegex,
  49. excludeServerRoutes = [],
  50. wrappingTargetKind,
  51. sentryConfigFilePath,
  52. vercelCronsConfig,
  53. nextjsRequestAsyncStorageModulePath,
  54. } = 'getOptions' in this ? this.getOptions() : this.query;
  55. this.async();
  56. let templateCode;
  57. if (wrappingTargetKind === 'sentry-init') {
  58. templateCode = sentryInitWrapperTemplateCode;
  59. // Absolute paths to the sentry config do not work with Windows: https://github.com/getsentry/sentry-javascript/issues/8133
  60. // Se we need check whether `this.resourcePath` is absolute because there is no contract by webpack that says it is absolute.
  61. // Examples where `this.resourcePath` could possibly be non-absolute are virtual modules.
  62. if (sentryConfigFilePath && path.isAbsolute(this.resourcePath)) {
  63. const sentryConfigImportPath = path
  64. .relative(path.dirname(this.resourcePath), sentryConfigFilePath)
  65. .replace(/\\/g, '/');
  66. // path.relative() may return something like `sentry.server.config.js` which is not allowed. Imports from the
  67. // current directory need to start with './'.This is why we prepend the path with './', which should always again
  68. // be a valid relative path.
  69. // https://github.com/getsentry/sentry-javascript/issues/8798
  70. templateCode = templateCode.replace(/__SENTRY_CONFIG_IMPORT_PATH__/g, `./${sentryConfigImportPath}`);
  71. } else {
  72. // Bail without doing any wrapping
  73. this.callback(null, userCode, userModuleSourceMap);
  74. return;
  75. }
  76. } else if (wrappingTargetKind === 'page' || wrappingTargetKind === 'api-route') {
  77. if (pagesDir === undefined) {
  78. this.callback(null, userCode, userModuleSourceMap);
  79. return;
  80. }
  81. // Get the parameterized route name from this page's filepath
  82. const parameterizedPagesRoute = path
  83. // Get the path of the file insde of the pages directory
  84. .relative(pagesDir, this.resourcePath)
  85. // Replace all backslashes with forward slashes (windows)
  86. .replace(/\\/g, '/')
  87. // Add a slash at the beginning
  88. .replace(/(.*)/, '/$1')
  89. // Pull off the file extension
  90. // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- not end user input
  91. .replace(new RegExp(`\\.(${pageExtensionRegex})`), '')
  92. // Any page file named `index` corresponds to root of the directory its in, URL-wise, so turn `/xyz/index` into
  93. // just `/xyz`
  94. .replace(/\/index$/, '')
  95. // In case all of the above have left us with an empty string (which will happen if we're dealing with the
  96. // homepage), sub back in the root route
  97. .replace(/^$/, '/');
  98. // Skip explicitly-ignored pages
  99. if (utils.stringMatchesSomePattern(parameterizedPagesRoute, excludeServerRoutes, true)) {
  100. this.callback(null, userCode, userModuleSourceMap);
  101. return;
  102. }
  103. if (wrappingTargetKind === 'page') {
  104. templateCode = pageWrapperTemplateCode;
  105. } else if (wrappingTargetKind === 'api-route') {
  106. templateCode = apiWrapperTemplateCode;
  107. } else {
  108. throw new Error(`Invariant: Could not get template code of unknown kind "${wrappingTargetKind}"`);
  109. }
  110. templateCode = templateCode.replace(/__VERCEL_CRONS_CONFIGURATION__/g, JSON.stringify(vercelCronsConfig));
  111. // Inject the route and the path to the file we're wrapping into the template
  112. templateCode = templateCode.replace(/__ROUTE__/g, parameterizedPagesRoute.replace(/\\/g, '\\\\'));
  113. } else if (wrappingTargetKind === 'server-component' || wrappingTargetKind === 'route-handler') {
  114. if (appDir === undefined) {
  115. this.callback(null, userCode, userModuleSourceMap);
  116. return;
  117. }
  118. // Get the parameterized route name from this page's filepath
  119. const parameterizedPagesRoute = path
  120. // Get the path of the file insde of the app directory
  121. .relative(appDir, this.resourcePath)
  122. // Replace all backslashes with forward slashes (windows)
  123. .replace(/\\/g, '/')
  124. // Add a slash at the beginning
  125. .replace(/(.*)/, '/$1')
  126. // Pull off the file name
  127. .replace(/\/[^/]+\.(js|ts|jsx|tsx)$/, '')
  128. // In case all of the above have left us with an empty string (which will happen if we're dealing with the
  129. // homepage), sub back in the root route
  130. .replace(/^$/, '/');
  131. // Skip explicitly-ignored pages
  132. if (utils.stringMatchesSomePattern(parameterizedPagesRoute, excludeServerRoutes, true)) {
  133. this.callback(null, userCode, userModuleSourceMap);
  134. return;
  135. }
  136. // The following string is what Next.js injects in order to mark client components:
  137. // https://github.com/vercel/next.js/blob/295f9da393f7d5a49b0c2e15a2f46448dbdc3895/packages/next/build/analysis/get-page-static-info.ts#L37
  138. // https://github.com/vercel/next.js/blob/a1c15d84d906a8adf1667332a3f0732be615afa0/packages/next-swc/crates/core/src/react_server_components.rs#L247
  139. // We do not want to wrap client components
  140. if (userCode.includes('__next_internal_client_entry_do_not_use__')) {
  141. this.callback(null, userCode, userModuleSourceMap);
  142. return;
  143. }
  144. if (wrappingTargetKind === 'server-component') {
  145. templateCode = serverComponentWrapperTemplateCode;
  146. } else {
  147. templateCode = routeHandlerWrapperTemplateCode;
  148. }
  149. if (nextjsRequestAsyncStorageModulePath !== undefined) {
  150. templateCode = templateCode.replace(
  151. /__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g,
  152. nextjsRequestAsyncStorageModulePath,
  153. );
  154. } else {
  155. if (!showedMissingAsyncStorageModuleWarning) {
  156. // eslint-disable-next-line no-console
  157. console.warn(
  158. `${chalk.yellow('warn')} - The Sentry SDK could not access the ${chalk.bold.cyan(
  159. 'RequestAsyncStorage',
  160. )} module. Certain features may not work. There is nothing you can do to fix this yourself, but future SDK updates may resolve this.\n`,
  161. );
  162. showedMissingAsyncStorageModuleWarning = true;
  163. }
  164. templateCode = templateCode.replace(
  165. /__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g,
  166. '@sentry/nextjs/esm/config/templates/requestAsyncStorageShim.js',
  167. );
  168. }
  169. templateCode = templateCode.replace(/__ROUTE__/g, parameterizedPagesRoute.replace(/\\/g, '\\\\'));
  170. const componentTypeMatch = path.posix
  171. .normalize(path.relative(appDir, this.resourcePath))
  172. .match(/\/?([^/]+)\.(?:js|ts|jsx|tsx)$/);
  173. if (componentTypeMatch && componentTypeMatch[1]) {
  174. let componentType;
  175. switch (componentTypeMatch[1]) {
  176. case 'page':
  177. componentType = 'Page';
  178. break;
  179. case 'layout':
  180. componentType = 'Layout';
  181. break;
  182. case 'head':
  183. componentType = 'Head';
  184. break;
  185. case 'not-found':
  186. componentType = 'Not-found';
  187. break;
  188. case 'loading':
  189. componentType = 'Loading';
  190. break;
  191. default:
  192. componentType = 'Unknown';
  193. }
  194. templateCode = templateCode.replace(/__COMPONENT_TYPE__/g, componentType);
  195. } else {
  196. templateCode = templateCode.replace(/__COMPONENT_TYPE__/g, 'Unknown');
  197. }
  198. } else if (wrappingTargetKind === 'middleware') {
  199. templateCode = middlewareWrapperTemplateCode;
  200. } else {
  201. throw new Error(`Invariant: Could not get template code of unknown kind "${wrappingTargetKind}"`);
  202. }
  203. // Replace the import path of the wrapping target in the template with a path that the `wrapUserCode` function will understand.
  204. templateCode = templateCode.replace(/__SENTRY_WRAPPING_TARGET_FILE__/g, WRAPPING_TARGET_MODULE_NAME);
  205. // Run the proxy module code through Rollup, in order to split the `export * from '<wrapped file>'` out into
  206. // individual exports (which nextjs seems to require).
  207. wrapUserCode(templateCode, userCode, userModuleSourceMap)
  208. .then(({ code: wrappedCode, map: wrappedCodeSourceMap }) => {
  209. this.callback(null, wrappedCode, wrappedCodeSourceMap);
  210. })
  211. .catch(err => {
  212. // eslint-disable-next-line no-console
  213. console.warn(
  214. `[@sentry/nextjs] Could not instrument ${this.resourcePath}. An error occurred while auto-wrapping:\n${err}`,
  215. );
  216. this.callback(null, userCode, userModuleSourceMap);
  217. });
  218. }
  219. /**
  220. * Use Rollup to process the proxy module code, in order to split its `export * from '<wrapped file>'` call into
  221. * individual exports (which nextjs seems to need).
  222. *
  223. * Wraps provided user code (located under the import defined via WRAPPING_TARGET_MODULE_NAME) with provided wrapper
  224. * code. Under the hood, this function uses rollup to bundle the modules together. Rollup is convenient for us because
  225. * it turns `export * from '<wrapped file>'` (which Next.js doesn't allow) into individual named exports.
  226. *
  227. * Note: This function may throw in case something goes wrong while bundling.
  228. *
  229. * @param wrapperCode The wrapper module code
  230. * @param userModuleCode The user module code
  231. * @returns The wrapped user code and a source map that describes the transformations done by this function
  232. */
  233. async function wrapUserCode(
  234. wrapperCode,
  235. userModuleCode,
  236. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  237. userModuleSourceMap,
  238. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  239. ) {
  240. const wrap = (withDefaultExport) =>
  241. rollup.rollup({
  242. input: SENTRY_WRAPPER_MODULE_NAME,
  243. plugins: [
  244. // We're using a simple custom plugin that virtualizes our wrapper module and the user module, so we don't have to
  245. // mess around with file paths and so that we can pass the original user module source map to rollup so that
  246. // rollup gives us a bundle with correct source mapping to the original file
  247. {
  248. name: 'virtualize-sentry-wrapper-modules',
  249. resolveId: id => {
  250. if (id === SENTRY_WRAPPER_MODULE_NAME || id === WRAPPING_TARGET_MODULE_NAME) {
  251. return id;
  252. } else {
  253. return null;
  254. }
  255. },
  256. load(id) {
  257. if (id === SENTRY_WRAPPER_MODULE_NAME) {
  258. return withDefaultExport ? wrapperCode : wrapperCode.replace('export { default } from', 'export {} from');
  259. } else if (id === WRAPPING_TARGET_MODULE_NAME) {
  260. return {
  261. code: userModuleCode,
  262. map: userModuleSourceMap, // give rollup acces to original user module source map
  263. };
  264. } else {
  265. return null;
  266. }
  267. },
  268. },
  269. // People may use `module.exports` in their API routes or page files. Next.js allows that and we also need to
  270. // handle that correctly so we let a plugin to take care of bundling cjs exports for us.
  271. commonjs.default({
  272. sourceMap: true,
  273. strictRequires: true, // Don't hoist require statements that users may define
  274. ignoreDynamicRequires: true, // Don't break dynamic requires and things like Webpack's `require.context`
  275. ignore() {
  276. // We basically only want to use this plugin for handling the case where users export their handlers with module.exports.
  277. // This plugin would also be able to convert any `require` into something esm compatible but webpack does that anyways so we just skip that part of the plugin.
  278. // (Also, modifying require may break user code)
  279. return true;
  280. },
  281. }),
  282. ],
  283. // We only want to bundle our wrapper module and the wrappee module into one, so we mark everything else as external.
  284. external: sourceId => sourceId !== SENTRY_WRAPPER_MODULE_NAME && sourceId !== WRAPPING_TARGET_MODULE_NAME,
  285. // Prevent rollup from stressing out about TS's use of global `this` when polyfilling await. (TS will polyfill if the
  286. // user's tsconfig `target` is set to anything before `es2017`. See https://stackoverflow.com/a/72822340 and
  287. // https://stackoverflow.com/a/60347490.)
  288. context: 'this',
  289. // Rollup's path-resolution logic when handling re-exports can go wrong when wrapping pages which aren't at the root
  290. // level of the `pages` directory. This may be a bug, as it doesn't match the behavior described in the docs, but what
  291. // seems to happen is this:
  292. //
  293. // - We try to wrap `pages/xyz/userPage.js`, which contains `export { helperFunc } from '../../utils/helper'`
  294. // - Rollup converts '../../utils/helper' into an absolute path
  295. // - We mark the helper module as external
  296. // - Rollup then converts it back to a relative path, but relative to `pages/` rather than `pages/xyz/`. (This is
  297. // the part which doesn't match the docs. They say that Rollup will use the common ancestor of all modules in the
  298. // bundle as the basis for the relative path calculation, but both our temporary file and the page being wrapped
  299. // live in `pages/xyz/`, and they're the only two files in the bundle, so `pages/xyz/`` should be used as the
  300. // root. Unclear why it's not.)
  301. // - As a result of the miscalculation, our proxy module will include `export { helperFunc } from '../utils/helper'`
  302. // rather than the expected `export { helperFunc } from '../../utils/helper'`, thereby causing a build error in
  303. // nextjs..
  304. //
  305. // Setting `makeAbsoluteExternalsRelative` to `false` prevents all of the above by causing Rollup to ignore imports of
  306. // externals entirely, with the result that their paths remain untouched (which is what we want).
  307. makeAbsoluteExternalsRelative: false,
  308. onwarn: (_warning, _warn) => {
  309. // Suppress all warnings - we don't want to bother people with this output
  310. // Might be stuff like "you have unused imports"
  311. // _warn(_warning); // uncomment to debug
  312. },
  313. });
  314. // Next.js sometimes complains if you define a default export (e.g. in route handlers in dev mode).
  315. // This is why we want to avoid unnecessarily creating default exports, even if they're just `undefined`.
  316. // For this reason we try to bundle/wrap the user code once including a re-export of `default`.
  317. // If the user code didn't have a default export, rollup will throw.
  318. // We then try bundling/wrapping agian, but without including a re-export of `default`.
  319. let rollupBuild;
  320. try {
  321. rollupBuild = await wrap(true);
  322. } catch (e) {
  323. if (_optionalChain([(e ), 'optionalAccess', _ => _.code]) === 'MISSING_EXPORT') {
  324. rollupBuild = await wrap(false);
  325. } else {
  326. throw e;
  327. }
  328. }
  329. const finalBundle = await rollupBuild.generate({
  330. format: 'esm',
  331. sourcemap: 'hidden', // put source map data in the bundle but don't generate a source map commment in the output
  332. });
  333. // The module at index 0 is always the entrypoint, which in this case is the proxy module.
  334. return finalBundle.output[0];
  335. }
  336. exports.default = wrappingLoader;
  337. //# sourceMappingURL=wrappingLoader.js.map