postinstall-fix.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. #!/usr/bin/env node
  2. /**
  3. * Postinstall fix: patch remove-scoped.js to guard unsafe `.spaces` access.
  4. *
  5. * Root cause: remove-scoped.js in @dcloudio/vue-cli-plugin-uni accesses
  6. * `selector.first.spaces.before` without checking if `selector.first` exists.
  7. * When postcss-selector-parser produces an empty selector (e.g. from certain
  8. * CSS edge cases), this crashes with:
  9. * "TypeError: Cannot read properties of undefined (reading 'spaces')"
  10. *
  11. * The fix adds a guard before accessing .spaces on selector.first.
  12. *
  13. * Patches BOTH:
  14. * 1. Project local node_modules (for vue-cli-service / npm build)
  15. * 2. HBuilderX bundled uniapp-cli (for HBuilderX build)
  16. */
  17. const fs = require('fs');
  18. const path = require('path');
  19. /**
  20. * Patch 2: templateLoader.js — remove undefined `recyclableRender` and `components` exports.
  21. *
  22. * Root cause: templateLoader.js hardcodes `export { render, staticRenderFns, recyclableRender, components }`
  23. * but the template compiler (vue-template-compiler / @vue/component-compiler-utils) does NOT define
  24. * `recyclableRender` or `components` in the output. This causes:
  25. * "Module parse failed: Export 'recyclableRender' is not defined"
  26. * "export 'components' was not found in './xxx.vue'"
  27. *
  28. * The fix removes the two undefined symbols from the export line.
  29. */
  30. function patchTemplateLoader(filePath) {
  31. let content = fs.readFileSync(filePath, 'utf8');
  32. // Check if already patched
  33. if (content.includes("export { render, staticRenderFns }") &&
  34. !content.includes("recyclableRender")) {
  35. console.log(` ⏭️ ${filePath} (templateLoader already patched)`);
  36. return true;
  37. }
  38. const oldExport = /export \{ render, staticRenderFns, recyclableRender, components \}/;
  39. if (oldExport.test(content)) {
  40. content = content.replace(
  41. oldExport,
  42. "export { render, staticRenderFns }"
  43. );
  44. fs.writeFileSync(filePath, content, 'utf8');
  45. console.log(` ✅ ${filePath}`);
  46. return true;
  47. }
  48. console.log(` ❌ ${filePath} (templateLoader unexpected format)`);
  49. return false;
  50. }
  51. /** Find all remove-scoped.js files to patch */
  52. function findRemoveScopedFiles() {
  53. const files = [];
  54. // 1. Project local node_modules
  55. const localPath = path.join(__dirname, 'node_modules', '@dcloudio',
  56. 'vue-cli-plugin-uni', 'packages', '@vue', 'component-compiler-utils',
  57. 'dist', 'stylePlugins', 'remove-scoped.js');
  58. if (fs.existsSync(localPath)) files.push(localPath);
  59. // 2. HBuilderX bundled uniapp-cli (common install paths)
  60. const hbxPaths = [
  61. 'E:\\develop\\HBuilderX\\plugins\\uniapp-cli',
  62. 'C:\\Program Files\\HBuilderX\\plugins\\uniapp-cli',
  63. 'C:\\Program Files (x86)\\HBuilderX\\plugins\\uniapp-cli',
  64. path.join(process.env.LOCALAPPDATA || '', 'Programs', 'HBuilderX', 'plugins', 'uniapp-cli'),
  65. ];
  66. for (const base of hbxPaths) {
  67. const hbxPath = path.join(base, 'node_modules', '@dcloudio',
  68. 'vue-cli-plugin-uni', 'packages', '@vue', 'component-compiler-utils',
  69. 'dist', 'stylePlugins', 'remove-scoped.js');
  70. if (fs.existsSync(hbxPath)) files.push(hbxPath);
  71. }
  72. return files;
  73. }
  74. /** Patch a single remove-scoped.js file */
  75. function patchFile(filePath) {
  76. let content = fs.readFileSync(filePath, 'utf8');
  77. // Fix: selector.first.spaces.before = ''; → if (selector.first) { ... }
  78. if (content.includes("selector.first.spaces.before = '';")) {
  79. content = content.replace(
  80. /selector\.first\.spaces\.before = '';/,
  81. "if (selector.first) { selector.first.spaces.before = ''; }"
  82. );
  83. fs.writeFileSync(filePath, content, 'utf8');
  84. console.log(` ✅ ${filePath}`);
  85. return true;
  86. } else if (content.includes("if (selector.first) { selector.first.spaces.before = ''; }")) {
  87. console.log(` ⏭️ ${filePath} (already patched)`);
  88. return true;
  89. } else {
  90. console.log(` ❌ ${filePath} (unexpected format)`);
  91. return false;
  92. }
  93. }
  94. // Main
  95. const files = findRemoveScopedFiles();
  96. if (files.length === 0) {
  97. console.log('remove-scoped.js not found in any location, skipping patch');
  98. } else {
  99. console.log(`Found ${files.length} remove-scoped.js to patch:`);
  100. let patched = 0;
  101. for (const f of files) {
  102. if (patchFile(f)) patched++;
  103. }
  104. console.log(`Done: ${patched}/${files.length} patched`);
  105. }
  106. // Patch 2: templateLoader.js
  107. const templateLoaderPath = path.join(__dirname, 'node_modules', '@dcloudio',
  108. 'vue-cli-plugin-uni', 'packages', 'vue-loader', 'lib', 'loaders', 'templateLoader.js');
  109. if (fs.existsSync(templateLoaderPath)) {
  110. console.log('templateLoader.js found, applying patch:');
  111. patchTemplateLoader(templateLoaderPath);
  112. } else {
  113. console.log('templateLoader.js not found, skipping patch');
  114. }