@automattic/i18n-check-webpack-plugin 1.0.35 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.0] - 2023-10-03
9
+ ### Added
10
+ - Add a sub-plugin, `I18nSafeMangleExportsPlugin`, to allow for avoiding problems with Webpack's `optimization.mangleExports` option occasionally mangling an export to one of the i18n function names. [#33392]
11
+
12
+ ## [1.0.36] - 2023-09-13
13
+ ### Changed
14
+ - Updated package dependencies. [#33001]
15
+
8
16
  ## [1.0.35] - 2023-09-04
9
17
  ### Changed
10
18
  - Updated package dependencies. [#32804]
@@ -168,6 +176,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
168
176
  ### Added
169
177
  - Initial release.
170
178
 
179
+ [1.1.0]: https://github.com/Automattic/i18n-check-webpack-plugin/compare/v1.0.36...v1.1.0
180
+ [1.0.36]: https://github.com/Automattic/i18n-check-webpack-plugin/compare/v1.0.35...v1.0.36
171
181
  [1.0.35]: https://github.com/Automattic/i18n-check-webpack-plugin/compare/v1.0.34...v1.0.35
172
182
  [1.0.34]: https://github.com/Automattic/i18n-check-webpack-plugin/compare/v1.0.33...v1.0.34
173
183
  [1.0.33]: https://github.com/Automattic/i18n-check-webpack-plugin/compare/v1.0.32...v1.0.33
package/README.md CHANGED
@@ -32,6 +32,31 @@ Parameters recognized by the plugin are:
32
32
  - `babelOptions`: Supply options for Babel.
33
33
  - `functions`: Supply a custom list of i18n functions to recognize.
34
34
 
35
+ ## Export mangling
36
+
37
+ Webpack's [optimization.mangleExports option](https://webpack.js.org/configuration/optimization/#optimizationmangleexports), enabled by default in production mode, can on occasion mangle an export to the name of one of the i18n functions this module looks for.
38
+
39
+ If you want export mangling and run into this issue, a slightly modified copy of Webpack's internal `MangleExportsPlugin` is provided. Require it as
40
+ ```
41
+ const I18nSafeMangleExportsPlugin = require( '@automattic/i18n-check-webpack-plugin/I18nSafeMangleExportsPlugin' );
42
+ ```
43
+ or
44
+ ```
45
+ import I18nSafeMangleExportsPlugin from '@automattic/i18n-check-webpack-plugin/I18nSafeMangleExportsPlugin';
46
+ ```
47
+ and add it to the `plugins` section of your Webpack config like
48
+ ```js
49
+ {
50
+ plugins: [
51
+ new I18nSafeMangleExportsPlugin(),
52
+ ],
53
+ };
54
+ ```
55
+
56
+ Parameters recognized by the plugin are:
57
+
58
+ - `deterministic`: Set false to use the 'size' mode.
59
+
35
60
  ## Known problematic code patterns
36
61
 
37
62
  These are some code patterns that are known to cause translations to be mangled.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/i18n-check-webpack-plugin",
3
- "version": "1.0.35",
3
+ "version": "1.1.0",
4
4
  "description": "A Webpack plugin to check that WordPress i18n hasn't been mangled by Webpack optimizations.",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/i18n-check-webpack-plugin/#readme",
6
6
  "bugs": {
@@ -20,7 +20,7 @@
20
20
  "debug": "^4.3.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@babel/core": "7.22.11",
23
+ "@babel/core": "7.22.17",
24
24
  "jest": "29.4.3",
25
25
  "webpack": "5.76.0",
26
26
  "webpack-cli": "4.9.1"
@@ -31,6 +31,7 @@
31
31
  },
32
32
  "exports": {
33
33
  ".": "./src/I18nCheckPlugin.js",
34
+ "./I18nSafeMangleExportsPlugin": "./src/I18nSafeMangleExportsPlugin.js",
34
35
  "./GettextExtractor": "./src/GettextExtractor.js",
35
36
  "./GettextEntries": "./src/GettextEntries.js",
36
37
  "./GettextEntry": "./src/GettextEntry.js"
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Webpack plugin to make export mangling safe to use with I18nCheckWebapckPlugin.
3
+ *
4
+ * Based on Webpack's MangleExportsPlugin. This is the same except for the
5
+ * addition of some default values in the `usedNames` set and the options
6
+ * accepted by the constructor.
7
+ */
8
+
9
+ const {
10
+ UsageState,
11
+ Template: {
12
+ numberToIdentifier,
13
+ NUMBER_OF_IDENTIFIER_START_CHARS,
14
+ NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS,
15
+ },
16
+ util: {
17
+ comparators: { compareSelect, compareStringsNumeric },
18
+ },
19
+ } = require( 'webpack' );
20
+ const { assignDeterministicIds } = require( 'webpack/lib/ids/IdHelpers' );
21
+
22
+ /** @typedef {import("../Compiler")} Compiler */
23
+ /** @typedef {import("../ExportsInfo")} ExportsInfo */
24
+ /** @typedef {import("../ExportsInfo").ExportInfo} ExportInfo */
25
+
26
+ /**
27
+ * Determine if we can mangle.
28
+ * @param {ExportsInfo} exportsInfo - exports info
29
+ * @returns {boolean} mangle is possible
30
+ */
31
+ const canMangle = exportsInfo => {
32
+ if ( exportsInfo.otherExportsInfo.getUsed( undefined ) !== UsageState.Unused ) {
33
+ return false;
34
+ }
35
+ let hasSomethingToMangle = false;
36
+ for ( const exportInfo of exportsInfo.exports ) {
37
+ if ( exportInfo.canMangle === true ) {
38
+ hasSomethingToMangle = true;
39
+ }
40
+ }
41
+ return hasSomethingToMangle;
42
+ };
43
+
44
+ // Sort by name
45
+ const comparator = compareSelect( e => e.name, compareStringsNumeric );
46
+ /**
47
+ * Mangle exports.
48
+ * @param {boolean} deterministic - use deterministic names
49
+ * @param {ExportsInfo} exportsInfo - exports info
50
+ * @param {boolean | undefined} isNamespace - is namespace object
51
+ * @returns {void}
52
+ */
53
+ const mangleExportsInfo = ( deterministic, exportsInfo, isNamespace ) => {
54
+ if ( ! canMangle( exportsInfo ) ) {
55
+ return;
56
+ }
57
+ const usedNames = new Set( [ '__', '_x', '_n', '_nx' ] );
58
+ /** @type {ExportInfo[]} */
59
+ const mangleableExports = [];
60
+
61
+ // Avoid to renamed exports that are not provided when
62
+ // 1. it's not a namespace export: non-provided exports can be found in prototype chain
63
+ // 2. there are other provided exports and deterministic mode is chosen:
64
+ // non-provided exports would break the determinism
65
+ let avoidMangleNonProvided = ! isNamespace;
66
+ if ( ! avoidMangleNonProvided && deterministic ) {
67
+ for ( const exportInfo of exportsInfo.ownedExports ) {
68
+ if ( exportInfo.provided !== false ) {
69
+ avoidMangleNonProvided = true;
70
+ break;
71
+ }
72
+ }
73
+ }
74
+ for ( const exportInfo of exportsInfo.ownedExports ) {
75
+ const name = exportInfo.name;
76
+ if ( ! exportInfo.hasUsedName() ) {
77
+ if (
78
+ // Can the export be mangled?
79
+ exportInfo.canMangle !== true ||
80
+ // Never rename 1 char exports
81
+ ( name.length === 1 && /^[a-zA-Z0-9_$]/.test( name ) ) ||
82
+ // Don't rename 2 char exports in deterministic mode
83
+ ( deterministic &&
84
+ name.length === 2 &&
85
+ /^[a-zA-Z_$][a-zA-Z0-9_$]|^[1-9][0-9]/.test( name ) ) ||
86
+ // Don't rename exports that are not provided
87
+ ( avoidMangleNonProvided && exportInfo.provided !== true )
88
+ ) {
89
+ exportInfo.setUsedName( name );
90
+ usedNames.add( name );
91
+ } else {
92
+ mangleableExports.push( exportInfo );
93
+ }
94
+ }
95
+ if ( exportInfo.exportsInfoOwned ) {
96
+ const used = exportInfo.getUsed( undefined );
97
+ if ( used === UsageState.OnlyPropertiesUsed || used === UsageState.Unused ) {
98
+ mangleExportsInfo( deterministic, exportInfo.exportsInfo, false );
99
+ }
100
+ }
101
+ }
102
+ if ( deterministic ) {
103
+ assignDeterministicIds(
104
+ mangleableExports,
105
+ e => e.name,
106
+ comparator,
107
+ ( e, id ) => {
108
+ const name = numberToIdentifier( id );
109
+ const size = usedNames.size;
110
+ usedNames.add( name );
111
+ if ( size === usedNames.size ) {
112
+ return false;
113
+ }
114
+ e.setUsedName( name );
115
+ return true;
116
+ },
117
+ [
118
+ NUMBER_OF_IDENTIFIER_START_CHARS,
119
+ NUMBER_OF_IDENTIFIER_START_CHARS * NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS,
120
+ ],
121
+ NUMBER_OF_IDENTIFIER_CONTINUATION_CHARS,
122
+ usedNames.size
123
+ );
124
+ } else {
125
+ const usedExports = [];
126
+ const unusedExports = [];
127
+ for ( const exportInfo of mangleableExports ) {
128
+ if ( exportInfo.getUsed( undefined ) === UsageState.Unused ) {
129
+ unusedExports.push( exportInfo );
130
+ } else {
131
+ usedExports.push( exportInfo );
132
+ }
133
+ }
134
+ usedExports.sort( comparator );
135
+ unusedExports.sort( comparator );
136
+ let i = 0;
137
+ for ( const list of [ usedExports, unusedExports ] ) {
138
+ for ( const exportInfo of list ) {
139
+ let name;
140
+ do {
141
+ name = numberToIdentifier( i++ );
142
+ } while ( usedNames.has( name ) );
143
+ exportInfo.setUsedName( name );
144
+ }
145
+ }
146
+ }
147
+ };
148
+
149
+ class MangleExportsPlugin {
150
+ constructor( options ) {
151
+ this._deterministic = options?.deterministic ?? true;
152
+ }
153
+ /**
154
+ * Apply the plugin
155
+ * @param {Compiler} compiler - the compiler instance
156
+ * @returns {void}
157
+ */
158
+ apply( compiler ) {
159
+ const { _deterministic: deterministic } = this;
160
+ compiler.hooks.compilation.tap( 'MangleExportsPlugin', compilation => {
161
+ const moduleGraph = compilation.moduleGraph;
162
+ compilation.hooks.optimizeCodeGeneration.tap( 'MangleExportsPlugin', modules => {
163
+ if ( compilation.moduleMemCaches ) {
164
+ throw new Error(
165
+ "optimization.mangleExports can't be used with cacheUnaffected as export mangling is a global effect"
166
+ );
167
+ }
168
+ for ( const module of modules ) {
169
+ const isNamespace = module.buildMeta && module.buildMeta.exportsType === 'namespace';
170
+ const exportsInfo = moduleGraph.getExportsInfo( module );
171
+ mangleExportsInfo( deterministic, exportsInfo, isNamespace );
172
+ }
173
+ } );
174
+ } );
175
+ }
176
+ }
177
+
178
+ module.exports = MangleExportsPlugin;