webpack-protect-bootstrap-plugin.js 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. const escapeStringRegexp = require('escape-string-regexp');
  2. const webpack = require('webpack');
  3. const G = webpack.RuntimeGlobals;
  4. const OBJ_RULE = [
  5. /([[(,=:]\s*{)(?!__proto__:)\s*(.)/g,
  6. (_, str, next) => `${str}__proto__: null${next === '}' ? '' : ','}${next}`,
  7. ];
  8. const BOOTSTRAP_RULES = [
  9. OBJ_RULE,
  10. [
  11. "typeof Symbol !== 'undefined' && Symbol.toStringTag",
  12. 'true',
  13. G.makeNamespaceObject,
  14. ], [
  15. 'Symbol.toStringTag',
  16. 'toStringTagSym',
  17. G.makeNamespaceObject,
  18. ], [
  19. 'Object.defineProperty(',
  20. 'defineProperty(',
  21. G.definePropertyGetters,
  22. ], [
  23. `${G.hasOwnProperty}(definition, key) && !${G.hasOwnProperty}(exports, key)`,
  24. '!(key in exports)',
  25. G.definePropertyGetters,
  26. ], [
  27. 'Object.prototype.hasOwnProperty.call(',
  28. 'safeCall(hasOwnProperty, ',
  29. G.hasOwnProperty,
  30. ],
  31. ];
  32. const MAIN_RULES = [
  33. [
  34. /(__webpack_modules__\[moduleId])\.call\(/g,
  35. 'safeCall($1, ',
  36. false,
  37. ], [
  38. new RegExp(`var (__webpack_module_cache__|${G.require}) = {};.*?var ${G.exports} =`, 's'),
  39. patchBootstrap,
  40. ], [
  41. new RegExp(`(${[
  42. `${G.definePropertyGetters}\\(${G.exports}, {`,
  43. `var ${G.exports} = {`,
  44. `var __webpack_modules__ = \\({`,
  45. ].join('|')})(?!__proto__:)\\s*(.)`, 'g'),
  46. OBJ_RULE[1],
  47. ],
  48. ];
  49. /**
  50. * WARNING! The following globals must be correctly assigned using wrapper-webpack-plugin.
  51. * toStringTagSym = Symbol.toStringTag
  52. * defineProperty = Object.defineProperty
  53. * hasOwnProperty = Object.prototype.hasOwnProperty
  54. * safeCall = Function.prototype.call.bind(Function.prototype.call)
  55. */
  56. class WebpackProtectBootstrapPlugin {
  57. apply(compiler) {
  58. const NAME = WebpackProtectBootstrapPlugin.name;
  59. compiler.hooks.compilation.tap(NAME, (compilation) => {
  60. const hooks = webpack.javascript.JavascriptModulesPlugin.getCompilationHooks(compilation);
  61. hooks.renderMain.tap(NAME, replace.bind(null, MAIN_RULES));
  62. });
  63. }
  64. }
  65. function patchBootstrap(src) {
  66. const props = src.match(new RegExp(`(?<=\\b${G.require}\\.)(\\w+)(?= = )`, 'g'));
  67. const guard = props
  68. ? `for (let i = 0, props="${[...new Set(props)].join('')}"; i < props.length; i++)
  69. defineProperty(${G.require}, props[i], {__proto__: null, value: 0, writable: 1});\n`
  70. : '';
  71. const res = replace(BOOTSTRAP_RULES, src, this);
  72. // splicing the guard after the first line to handle `require = {}`
  73. const i = res.indexOf('\n');
  74. return res.slice(0, i) + guard + res.slice(i);
  75. }
  76. function replace(rules, src, info) {
  77. src = src.source?.() || src;
  78. let res = src;
  79. for (const rule of rules) {
  80. const [from, to, test = true] = rule;
  81. const fromRe = typeof from === 'string'
  82. ? new RegExp(escapeStringRegexp(from), 'g')
  83. : from;
  84. const dst = res.replace(fromRe, to.bind?.(info) || to);
  85. const mandatory = test === true
  86. || test.test?.(src)
  87. || typeof test === 'string' && src.includes(test);
  88. if (dst === res && mandatory) {
  89. const err = `[${WebpackProtectBootstrapPlugin.name}] `
  90. + `"${from}" not found in ${info.chunk.name || 'bootstrap'}`;
  91. console.log(`${err}:\n${src}`); // this prints immediately
  92. throw new Error(err); // this prints at the end of build
  93. }
  94. res = dst;
  95. }
  96. return res;
  97. }
  98. module.exports = WebpackProtectBootstrapPlugin;