Repository: webpack/webpack (JavaScript)

Issue:
Normalize property access for imports and exports
## Feature request

Currently webpack exports using quote notation and imports using dot notation.

e.g., look at the use of the "*a*" property:
```
...
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */   "a": function() { return /* binding */ ActionOperation; }
/* harmony export */ });
...
var ActionOperation = __webpack_require__(86792);
class ProcessActionOperation extends ActionOperation/* ActionOperation */.a {
...
```

Some optimizing compilers such as the Google Closure Compiler see these as different.  They use the quote notation as a way to say, "hands off" when it comes to minifying variable names so that external interfaces can be preserved.  This leads to it seeing x["somevar"] and x.somevar as two different properties, such that x.somevar is minified and x["somevar"] is converted to x.somevar (shorter by one char but not minified).  See [the Closure docs](https://developers.google.com/closure/compiler/docs/api-tutorial3#propnames) for more.

**What is the expected behavior?**

It's important for webpack to be consistent in the generation and consumption of properties - either dot notation or quote notation.  This was mostly OK in webpack 4; webpack 5 introduced breaking changes.

**What is motivation or use case for adding/changing the behavior?**

Get webpack 5 to generate output compatible with the Google Closure Compiler.

**How should this be implemented in your opinion?**

The proposed change is relatively straightforward.  A new method `propertyName` will be introduced using similar logic to `propertyAccess`.  This will be used instead of JSON.stringify when creating property names.

**Are you willing to work on this yourself?**

Yes



Hints:
PR welcome

Patch:
diff --git a/lib/dependencies/HarmonyExportExpressionDependency.js b/lib/dependencies/HarmonyExportExpressionDependency.js
--- a/lib/dependencies/HarmonyExportExpressionDependency.js
+++ b/lib/dependencies/HarmonyExportExpressionDependency.js
@@ -8,6 +8,7 @@
 const ConcatenationScope = require("../ConcatenationScope");
 const RuntimeGlobals = require("../RuntimeGlobals");
 const makeSerializable = require("../util/makeSerializable");
+const propertyAccess = require("../util/propertyAccess");
 const HarmonyExportInitFragment = require("./HarmonyExportInitFragment");
 const NullDependency = require("./NullDependency");
 
@@ -172,9 +173,9 @@ HarmonyExportExpressionDependency.Template = class HarmonyExportDependencyTempla
 				if (used) {
 					runtimeRequirements.add(RuntimeGlobals.exports);
 					// This is a little bit incorrect as TDZ is not correct, but we can't use const.
-					content = `/* harmony default export */ ${exportsName}[${JSON.stringify(
+					content = `/* harmony default export */ ${exportsName}${propertyAccess(
 						used
-					)}] = `;
+					)} = `;
 				} else {
 					content = `/* unused harmony default export */ var ${name} = `;
 				}
diff --git a/lib/dependencies/HarmonyExportImportedSpecifierDependency.js b/lib/dependencies/HarmonyExportImportedSpecifierDependency.js
--- a/lib/dependencies/HarmonyExportImportedSpecifierDependency.js
+++ b/lib/dependencies/HarmonyExportImportedSpecifierDependency.js
@@ -15,6 +15,7 @@ const { countIterable } = require("../util/IterableHelpers");
 const { first, combine } = require("../util/SetHelpers");
 const makeSerializable = require("../util/makeSerializable");
 const propertyAccess = require("../util/propertyAccess");
+const { propertyName } = require("../util/propertyName");
 const { getRuntimeKey, keyToRuntime } = require("../util/runtime");
 const HarmonyExportInitFragment = require("./HarmonyExportInitFragment");
 const HarmonyImportDependency = require("./HarmonyImportDependency");
@@ -1219,7 +1220,7 @@ HarmonyExportImportedSpecifierDependency.Template = class HarmonyExportImportedS
 			valueKey[0]
 		)})) ${
 			RuntimeGlobals.definePropertyGetters
-		}(${exportsName}, { ${JSON.stringify(
+		}(${exportsName}, { ${propertyName(
 			key
 		)}: function() { return ${returnValue}; } });\n`;
 	}
diff --git a/lib/dependencies/HarmonyExportInitFragment.js b/lib/dependencies/HarmonyExportInitFragment.js
--- a/lib/dependencies/HarmonyExportInitFragment.js
+++ b/lib/dependencies/HarmonyExportInitFragment.js
@@ -8,6 +8,7 @@
 const InitFragment = require("../InitFragment");
 const RuntimeGlobals = require("../RuntimeGlobals");
 const { first } = require("../util/SetHelpers");
+const { propertyName } = require("../util/propertyName");
 
 /** @typedef {import("webpack-sources").Source} Source */
 /** @typedef {import("../Generator").GenerateContext} GenerateContext */
@@ -150,7 +151,7 @@ class HarmonyExportInitFragment extends InitFragment {
 		);
 		for (const [key, value] of orderedExportMap) {
 			definitions.push(
-				`\n/* harmony export */   ${JSON.stringify(
+				`\n/* harmony export */   ${propertyName(
 					key
 				)}: ${runtimeTemplate.returningFunction(value)}`
 			);
diff --git a/lib/optimize/ConcatenatedModule.js b/lib/optimize/ConcatenatedModule.js
--- a/lib/optimize/ConcatenatedModule.js
+++ b/lib/optimize/ConcatenatedModule.js
@@ -27,6 +27,7 @@ const createHash = require("../util/createHash");
 const { makePathsRelative } = require("../util/identifier");
 const makeSerializable = require("../util/makeSerializable");
 const propertyAccess = require("../util/propertyAccess");
+const { propertyName } = require("../util/propertyName");
 const {
 	filterRuntime,
 	intersectRuntime,
@@ -1484,7 +1485,7 @@ class ConcatenatedModule extends Module {
 			const definitions = [];
 			for (const [key, value] of exportsMap) {
 				definitions.push(
-					`\n  ${JSON.stringify(key)}: ${runtimeTemplate.returningFunction(
+					`\n  ${propertyName(key)}: ${runtimeTemplate.returningFunction(
 						value(requestShortener)
 					)}`
 				);
@@ -1529,9 +1530,9 @@ class ConcatenatedModule extends Module {
 						true
 					);
 					nsObj.push(
-						`\n  ${JSON.stringify(
-							usedName
-						)}: ${runtimeTemplate.returningFunction(finalName)}`
+						`\n  ${propertyName(usedName)}: ${runtimeTemplate.returningFunction(
+							finalName
+						)}`
 					);
 				}
 			}
diff --git a/lib/util/propertyAccess.js b/lib/util/propertyAccess.js
--- a/lib/util/propertyAccess.js
+++ b/lib/util/propertyAccess.js
@@ -5,60 +5,10 @@
 
 "use strict";
 
-const SAFE_IDENTIFIER = /^[_a-zA-Z$][_a-zA-Z$0-9]*$/;
-const RESERVED_IDENTIFIER = new Set([
-	"break",
-	"case",
-	"catch",
-	"class",
-	"const",
-	"continue",
-	"debugger",
-	"default",
-	"delete",
-	"do",
-	"else",
-	"export",
-	"extends",
-	"finally",
-	"for",
-	"function",
-	"if",
-	"import",
-	"in",
-	"instanceof",
-	"new",
-	"return",
-	"super",
-	"switch",
-	"this",
-	"throw",
-	"try",
-	"typeof",
-	"var",
-	"void",
-	"while",
-	"with",
-	"enum",
-	// strict mode
-	"implements",
-	"interface",
-	"let",
-	"package",
-	"private",
-	"protected",
-	"public",
-	"static",
-	"yield",
-	"yield",
-	// module code
-	"await",
-	// skip future reserved keywords defined under ES1 till ES3
-	// additional
-	"null",
-	"true",
-	"false"
-]);
+const {
+	SAFE_IDENTIFIER,
+	RESERVED_IDENTIFIER
+} = require("../util/propertyName");
 
 /**
  * @param {ArrayLike<string>} properties properties
diff --git a/lib/util/propertyName.js b/lib/util/propertyName.js
new file mode 100644
--- /dev/null
+++ b/lib/util/propertyName.js
@@ -0,0 +1,79 @@
+/*
+	MIT License http://www.opensource.org/licenses/mit-license.php
+	Author Tobias Koppers @sokra
+*/
+
+"use strict";
+
+const SAFE_IDENTIFIER = /^[_a-zA-Z$][_a-zA-Z$0-9]*$/;
+const RESERVED_IDENTIFIER = new Set([
+	"break",
+	"case",
+	"catch",
+	"class",
+	"const",
+	"continue",
+	"debugger",
+	"default",
+	"delete",
+	"do",
+	"else",
+	"export",
+	"extends",
+	"finally",
+	"for",
+	"function",
+	"if",
+	"import",
+	"in",
+	"instanceof",
+	"new",
+	"return",
+	"super",
+	"switch",
+	"this",
+	"throw",
+	"try",
+	"typeof",
+	"var",
+	"void",
+	"while",
+	"with",
+	"enum",
+	// strict mode
+	"implements",
+	"interface",
+	"let",
+	"package",
+	"private",
+	"protected",
+	"public",
+	"static",
+	"yield",
+	"yield",
+	// module code
+	"await",
+	// skip future reserved keywords defined under ES1 till ES3
+	// additional
+	"null",
+	"true",
+	"false"
+]);
+
+/**
+ * @summary Returns a valid JS property name for the given property.
+ * Certain strings like "default", "null", and names with whitespace are not
+ * valid JS property names, so they are returned as strings.
+ *
+ * @param {string} prop property name to analyze
+ * @returns {string} valid JS property name
+ */
+const propertyName = prop => {
+	if (SAFE_IDENTIFIER.test(prop) && !RESERVED_IDENTIFIER.has(prop)) {
+		return prop;
+	} else {
+		return JSON.stringify(prop);
+	}
+};
+
+module.exports = { SAFE_IDENTIFIER, RESERVED_IDENTIFIER, propertyName };


Test patch:
diff --git a/test/__snapshots__/StatsTestCases.basictest.js.snap b/test/__snapshots__/StatsTestCases.basictest.js.snap
--- a/test/__snapshots__/StatsTestCases.basictest.js.snap
+++ b/test/__snapshots__/StatsTestCases.basictest.js.snap
@@ -306,9 +306,9 @@ default:
 
 vendors:
   Entrypoint main 11.1 KiB = vendors/main.js
-  Entrypoint a 14.5 KiB = vendors/vendors.js 1.05 KiB vendors/a.js 13.5 KiB
-  Entrypoint b 8.18 KiB = vendors/vendors.js 1.05 KiB vendors/b.js 7.13 KiB
-  Entrypoint c 8.18 KiB = vendors/vendors.js 1.05 KiB vendors/c.js 7.13 KiB
+  Entrypoint a 14.5 KiB = vendors/vendors.js 1.04 KiB vendors/a.js 13.5 KiB
+  Entrypoint b 8.17 KiB = vendors/vendors.js 1.04 KiB vendors/b.js 7.13 KiB
+  Entrypoint c 8.17 KiB = vendors/vendors.js 1.04 KiB vendors/c.js 7.13 KiB
   chunk (runtime: b) vendors/b.js (b) 156 bytes (javascript) 2.75 KiB (runtime) [entry] [rendered]
     > ./b b
     runtime modules 2.75 KiB 4 modules
@@ -355,9 +355,9 @@ vendors:
 
 multiple-vendors:
   Entrypoint main 11.5 KiB = multiple-vendors/main.js
-  Entrypoint a 15 KiB = multiple-vendors/libs-x.js 414 bytes multiple-vendors/954.js 414 bytes multiple-vendors/767.js 414 bytes multiple-vendors/390.js 414 bytes multiple-vendors/a.js 13.4 KiB
-  Entrypoint b 8.14 KiB = multiple-vendors/libs-x.js 414 bytes multiple-vendors/954.js 414 bytes multiple-vendors/767.js 414 bytes multiple-vendors/568.js 414 bytes multiple-vendors/b.js 6.52 KiB
-  Entrypoint c 8.14 KiB = multiple-vendors/libs-x.js 414 bytes multiple-vendors/769.js 414 bytes multiple-vendors/767.js 414 bytes multiple-vendors/568.js 414 bytes multiple-vendors/c.js 6.52 KiB
+  Entrypoint a 15 KiB = multiple-vendors/libs-x.js 412 bytes multiple-vendors/954.js 412 bytes multiple-vendors/767.js 412 bytes multiple-vendors/390.js 412 bytes multiple-vendors/a.js 13.4 KiB
+  Entrypoint b 8.13 KiB = multiple-vendors/libs-x.js 412 bytes multiple-vendors/954.js 412 bytes multiple-vendors/767.js 412 bytes multiple-vendors/568.js 412 bytes multiple-vendors/b.js 6.52 KiB
+  Entrypoint c 8.13 KiB = multiple-vendors/libs-x.js 412 bytes multiple-vendors/769.js 412 bytes multiple-vendors/767.js 412 bytes multiple-vendors/568.js 412 bytes multiple-vendors/c.js 6.52 KiB
   chunk (runtime: a, b, c, main) multiple-vendors/libs-x.js (libs-x) (id hint: libs) 20 bytes [initial] [rendered] split chunk (cache group: libs) (name: libs-x)
     > ./a ./index.js 1:0-47
     > ./b ./index.js 2:0-47
@@ -427,9 +427,9 @@ multiple-vendors:
 
 all:
   Entrypoint main 11.5 KiB = all/main.js
-  Entrypoint a 15 KiB = all/282.js 414 bytes all/954.js 414 bytes all/767.js 414 bytes all/390.js 414 bytes all/a.js 13.4 KiB
-  Entrypoint b 8.14 KiB = all/282.js 414 bytes all/954.js 414 bytes all/767.js 414 bytes all/568.js 414 bytes all/b.js 6.52 KiB
-  Entrypoint c 8.14 KiB = all/282.js 414 bytes all/769.js 414 bytes all/767.js 414 bytes all/568.js 414 bytes all/c.js 6.52 KiB
+  Entrypoint a 15 KiB = all/282.js 412 bytes all/954.js 412 bytes all/767.js 412 bytes all/390.js 412 bytes all/a.js 13.4 KiB
+  Entrypoint b 8.13 KiB = all/282.js 412 bytes all/954.js 412 bytes all/767.js 412 bytes all/568.js 412 bytes all/b.js 6.52 KiB
+  Entrypoint c 8.13 KiB = all/282.js 412 bytes all/769.js 412 bytes all/767.js 412 bytes all/568.js 412 bytes all/c.js 6.52 KiB
   chunk (runtime: b) all/b.js (b) 116 bytes (javascript) 2.76 KiB (runtime) [entry] [rendered]
     > ./b b
     runtime modules 2.76 KiB 4 modules
@@ -710,8 +710,8 @@ webpack x.x.x compiled successfully in X ms"
 
 exports[`StatsTestCases should print correct stats for commons-plugin-issue-4980 1`] = `
 "asset app.a304ced30e50efdd246d-1.js 6.24 KiB [emitted] [immutable] (name: app)
-asset vendor.e8705eba33f92df1cf62-1.js 619 bytes [emitted] [immutable] (name: vendor) (id hint: vendor)
-Entrypoint app 6.84 KiB = vendor.e8705eba33f92df1cf62-1.js 619 bytes app.a304ced30e50efdd246d-1.js 6.24 KiB
+asset vendor.e8705eba33f92df1cf62-1.js 611 bytes [emitted] [immutable] (name: vendor) (id hint: vendor)
+Entrypoint app 6.83 KiB = vendor.e8705eba33f92df1cf62-1.js 611 bytes app.a304ced30e50efdd246d-1.js 6.24 KiB
 runtime modules 2.75 KiB 4 modules
 orphan modules 118 bytes [orphan] 2 modules
 cacheable modules 272 bytes
@@ -720,8 +720,8 @@ cacheable modules 272 bytes
 webpack x.x.x compiled successfully in X ms
 
 asset app.8f403eca7a1e59a7ce89-2.js 6.25 KiB [emitted] [immutable] (name: app)
-asset vendor.e8705eba33f92df1cf62-2.js 619 bytes [emitted] [immutable] (name: vendor) (id hint: vendor)
-Entrypoint app 6.86 KiB = vendor.e8705eba33f92df1cf62-2.js 619 bytes app.8f403eca7a1e59a7ce89-2.js 6.25 KiB
+asset vendor.e8705eba33f92df1cf62-2.js 611 bytes [emitted] [immutable] (name: vendor) (id hint: vendor)
+Entrypoint app 6.85 KiB = vendor.e8705eba33f92df1cf62-2.js 611 bytes app.8f403eca7a1e59a7ce89-2.js 6.25 KiB
 runtime modules 2.75 KiB 4 modules
 orphan modules 125 bytes [orphan] 2 modules
 cacheable modules 279 bytes
@@ -1633,9 +1633,9 @@ exports[`StatsTestCases should print correct stats for module-deduplication 1`]
 "asset e1.js 12.2 KiB [emitted] (name: e1)
 asset e2.js 12.2 KiB [emitted] (name: e2)
 asset e3.js 12.2 KiB [emitted] (name: e3)
-asset 172.js 858 bytes [emitted]
-asset 326.js 858 bytes [emitted]
-asset 923.js 858 bytes [emitted]
+asset 172.js 856 bytes [emitted]
+asset 326.js 856 bytes [emitted]
+asset 923.js 856 bytes [emitted]
 asset 114.js 524 bytes [emitted]
 asset 593.js 524 bytes [emitted]
 asset 716.js 524 bytes [emitted]
@@ -1676,12 +1676,12 @@ webpack x.x.x compiled successfully"
 `;
 
 exports[`StatsTestCases should print correct stats for module-deduplication-named 1`] = `
-"asset e1.js 12.1 KiB [emitted] (name: e1)
-asset e2.js 12.1 KiB [emitted] (name: e2)
-asset e3.js 12.1 KiB [emitted] (name: e3)
-asset async1.js 964 bytes [emitted] (name: async1)
-asset async2.js 964 bytes [emitted] (name: async2)
-asset async3.js 964 bytes [emitted] (name: async3)
+"asset e1.js 12 KiB [emitted] (name: e1)
+asset e2.js 12 KiB [emitted] (name: e2)
+asset e3.js 12 KiB [emitted] (name: e3)
+asset async1.js 962 bytes [emitted] (name: async1)
+asset async2.js 962 bytes [emitted] (name: async2)
+asset async3.js 962 bytes [emitted] (name: async3)
 chunk (runtime: e3) e3.js (e3) 242 bytes (javascript) 6.63 KiB (runtime) [entry] [rendered]
   runtime modules 6.63 KiB 9 modules
   cacheable modules 242 bytes
@@ -1816,7 +1816,7 @@ exports[`StatsTestCases should print correct stats for named-chunk-groups 1`] =
 "Chunk Group main 11.7 KiB = a-main.js
 Chunk Group async-a 1.07 KiB = a-52.js 257 bytes a-async-a.js 836 bytes
 Chunk Group async-b 1.07 KiB = a-52.js 257 bytes a-async-b.js 836 bytes
-Chunk Group async-c 1.45 KiB = a-vendors.js 744 bytes a-async-c.js 741 bytes
+Chunk Group async-c 1.45 KiB = a-vendors.js 740 bytes a-async-c.js 741 bytes
 chunk (runtime: main) a-52.js 149 bytes [rendered] split chunk (cache group: default)
   > ./a ./index.js 1:0-47
   > ./b ./index.js 2:0-47
@@ -1843,7 +1843,7 @@ webpack x.x.x compiled successfully
 Entrypoint main 11.7 KiB = b-main.js
 Chunk Group async-a 1.07 KiB = b-52.js 257 bytes b-async-a.js 836 bytes
 Chunk Group async-b 1.07 KiB = b-52.js 257 bytes b-async-b.js 836 bytes
-Chunk Group async-c 1.45 KiB = b-vendors.js 744 bytes b-async-c.js 741 bytes
+Chunk Group async-c 1.45 KiB = b-vendors.js 740 bytes b-async-c.js 741 bytes
 chunk (runtime: main) b-52.js 149 bytes [rendered] split chunk (cache group: default)
   > ./a ./index.js 1:0-47
   > ./b ./index.js 2:0-47
@@ -1954,7 +1954,7 @@ webpack x.x.x compiled successfully in X ms"
 
 exports[`StatsTestCases should print correct stats for output-module 1`] = `
 "asset main.mjs 9.53 KiB [emitted] [javascript module] (name: main)
-asset 52.mjs 358 bytes [emitted] [javascript module]
+asset 52.mjs 356 bytes [emitted] [javascript module]
 runtime modules 5.76 KiB 7 modules
 orphan modules 38 bytes [orphan] 1 module
 cacheable modules 263 bytes
@@ -3066,13 +3066,13 @@ webpack x.x.x compiled successfully"
 
 exports[`StatsTestCases should print correct stats for runtime-specific-used-exports 1`] = `
 "production:
-  asset production-a.js 13.2 KiB [emitted] (name: a)
-  asset production-b.js 13.2 KiB [emitted] (name: b)
-  asset production-dx_js.js 1.16 KiB [emitted]
-  asset production-dw_js-_a6170.js 1.16 KiB [emitted]
-  asset production-dw_js-_a6171.js 1.16 KiB [emitted]
-  asset production-dy_js.js 1.14 KiB [emitted]
-  asset production-dz_js.js 1.14 KiB [emitted]
+  asset production-a.js 13.1 KiB [emitted] (name: a)
+  asset production-b.js 13.1 KiB [emitted] (name: b)
+  asset production-dw_js-_a6170.js 1.15 KiB [emitted]
+  asset production-dw_js-_a6171.js 1.15 KiB [emitted]
+  asset production-dx_js.js 1.15 KiB [emitted]
+  asset production-dy_js.js 1.13 KiB [emitted]
+  asset production-dz_js.js 1.13 KiB [emitted]
   asset production-c.js 93 bytes [emitted] (name: c)
   chunk (runtime: a) production-a.js (a) 605 bytes (javascript) 6.59 KiB (runtime) [entry] [rendered]
     runtime modules 6.59 KiB 9 modules
@@ -3148,12 +3148,12 @@ exports[`StatsTestCases should print correct stats for runtime-specific-used-exp
   production (webpack x.x.x) compiled successfully in X ms
 
 development:
-  asset development-a.js 15.9 KiB [emitted] (name: a)
-  asset development-b.js 15.9 KiB [emitted] (name: b)
-  asset development-dw_js.js 2.11 KiB [emitted]
-  asset development-dx_js.js 2.11 KiB [emitted]
-  asset development-dy_js.js 2.11 KiB [emitted]
-  asset development-dz_js.js 2.11 KiB [emitted]
+  asset development-a.js 15.8 KiB [emitted] (name: a)
+  asset development-b.js 15.8 KiB [emitted] (name: b)
+  asset development-dw_js.js 2.09 KiB [emitted]
+  asset development-dx_js.js 2.09 KiB [emitted]
+  asset development-dy_js.js 2.09 KiB [emitted]
+  asset development-dz_js.js 2.09 KiB [emitted]
   asset development-c.js 1.13 KiB [emitted] (name: c)
   chunk (runtime: a) development-a.js (a) 605 bytes (javascript) 6.59 KiB (runtime) [entry] [rendered]
     runtime modules 6.59 KiB 9 modules
@@ -3235,10 +3235,10 @@ development:
 global:
   asset global-a.js 13.3 KiB [emitted] (name: a)
   asset global-b.js 13.3 KiB [emitted] (name: b)
-  asset global-dw_js.js 1.16 KiB [emitted]
-  asset global-dx_js.js 1.16 KiB [emitted]
-  asset global-dy_js.js 1.16 KiB [emitted]
-  asset global-dz_js.js 1.16 KiB [emitted]
+  asset global-dw_js.js 1.15 KiB [emitted]
+  asset global-dx_js.js 1.15 KiB [emitted]
+  asset global-dy_js.js 1.15 KiB [emitted]
+  asset global-dz_js.js 1.15 KiB [emitted]
   asset global-c.js 93 bytes [emitted] (name: c)
   chunk (runtime: a) global-a.js (a) 605 bytes (javascript) 6.59 KiB (runtime) [entry] [rendered]
     runtime modules 6.59 KiB 9 modules
@@ -3347,8 +3347,8 @@ webpack x.x.x compiled successfully in X ms"
 `;
 
 exports[`StatsTestCases should print correct stats for scope-hoisting-multi 1`] = `
-"Entrypoint first 14.4 KiB = a-vendor.js 419 bytes a-first.js 14 KiB
-Entrypoint second 13.9 KiB = a-vendor.js 419 bytes a-second.js 13.5 KiB
+"Entrypoint first 14.4 KiB = a-vendor.js 417 bytes a-first.js 14 KiB
+Entrypoint second 13.9 KiB = a-vendor.js 417 bytes a-second.js 13.5 KiB
 runtime modules 15.2 KiB 20 modules
 orphan modules 37 bytes [orphan] 1 module
 cacheable modules 807 bytes
@@ -3364,8 +3364,8 @@ cacheable modules 807 bytes
   ./common_lazy_shared.js 25 bytes [built] [code generated]
 webpack x.x.x compiled successfully in X ms
 
-Entrypoint first 13.7 KiB = b-vendor.js 419 bytes b-first.js 13.3 KiB
-Entrypoint second 13.5 KiB = b-vendor.js 419 bytes b-second.js 13.1 KiB
+Entrypoint first 13.7 KiB = b-vendor.js 417 bytes b-first.js 13.3 KiB
+Entrypoint second 13.5 KiB = b-vendor.js 417 bytes b-second.js 13.1 KiB
 runtime modules 15.2 KiB 20 modules
 cacheable modules 975 bytes
   code generated modules 857 bytes [code generated]
@@ -3570,8 +3570,8 @@ exports[`StatsTestCases should print correct stats for split-chunks 1`] = `
 "default:
   Entrypoint main 11.5 KiB = default/main.js
   Entrypoint a 12.6 KiB = default/a.js
-  Entrypoint b 3.94 KiB = default/b.js
-  Entrypoint c 3.94 KiB = default/c.js
+  Entrypoint b 3.93 KiB = default/b.js
+  Entrypoint c 3.93 KiB = default/c.js
   chunk (runtime: b) default/b.js (b) 196 bytes (javascript) 396 bytes (runtime) [entry] [rendered]
     > ./b b
     dependent modules 80 bytes [dependent] 4 modules
@@ -3629,9 +3629,9 @@ exports[`StatsTestCases should print correct stats for split-chunks 1`] = `
 
 all-chunks:
   Entrypoint main 11.5 KiB = all-chunks/main.js
-  Entrypoint a 15 KiB = all-chunks/282.js 414 bytes all-chunks/954.js 414 bytes all-chunks/767.js 414 bytes all-chunks/390.js 414 bytes all-chunks/a.js 13.4 KiB
-  Entrypoint b 8.14 KiB = all-chunks/282.js 414 bytes all-chunks/954.js 414 bytes all-chunks/767.js 414 bytes all-chunks/568.js 414 bytes all-chunks/b.js 6.52 KiB
-  Entrypoint c 8.14 KiB = all-chunks/282.js 414 bytes all-chunks/769.js 414 bytes all-chunks/767.js 414 bytes all-chunks/568.js 414 bytes all-chunks/c.js 6.52 KiB
+  Entrypoint a 15 KiB = all-chunks/282.js 412 bytes all-chunks/954.js 412 bytes all-chunks/767.js 412 bytes all-chunks/390.js 412 bytes all-chunks/a.js 13.4 KiB
+  Entrypoint b 8.13 KiB = all-chunks/282.js 412 bytes all-chunks/954.js 412 bytes all-chunks/767.js 412 bytes all-chunks/568.js 412 bytes all-chunks/b.js 6.52 KiB
+  Entrypoint c 8.13 KiB = all-chunks/282.js 412 bytes all-chunks/769.js 412 bytes all-chunks/767.js 412 bytes all-chunks/568.js 412 bytes all-chunks/c.js 6.52 KiB
   chunk (runtime: b) all-chunks/b.js (b) 116 bytes (javascript) 2.76 KiB (runtime) ={282}= ={568}= ={767}= ={954}= [entry] [rendered]
     > ./b b
     runtime modules 2.76 KiB 4 modules
@@ -3701,9 +3701,9 @@ all-chunks:
 
 manual:
   Entrypoint main 11.3 KiB = manual/main.js
-  Entrypoint a 14.8 KiB = manual/vendors.js 1.05 KiB manual/a.js 13.7 KiB
-  Entrypoint b 8.45 KiB = manual/vendors.js 1.05 KiB manual/b.js 7.4 KiB
-  Entrypoint c 8.45 KiB = manual/vendors.js 1.05 KiB manual/c.js 7.4 KiB
+  Entrypoint a 14.8 KiB = manual/vendors.js 1.04 KiB manual/a.js 13.7 KiB
+  Entrypoint b 8.44 KiB = manual/vendors.js 1.04 KiB manual/b.js 7.39 KiB
+  Entrypoint c 8.44 KiB = manual/vendors.js 1.04 KiB manual/c.js 7.39 KiB
   chunk (runtime: b) manual/b.js (b) 156 bytes (javascript) 2.76 KiB (runtime) ={216}= [entry] [rendered]
     > ./b b
     > x b
@@ -3771,9 +3771,9 @@ manual:
 
 name-too-long:
   Entrypoint main 11.5 KiB = name-too-long/main.js
-  Entrypoint aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 15 KiB = name-too-long/282.js 414 bytes name-too-long/954.js 414 bytes name-too-long/767.js 414 bytes name-too-long/390.js 414 bytes name-too-long/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.js 13.4 KiB
-  Entrypoint bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 8.14 KiB = name-too-long/282.js 414 bytes name-too-long/954.js 414 bytes name-too-long/767.js 414 bytes name-too-long/568.js 414 bytes name-too-long/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.js 6.52 KiB
-  Entrypoint cccccccccccccccccccccccccccccc 8.14 KiB = name-too-long/282.js 414 bytes name-too-long/769.js 414 bytes name-too-long/767.js 414 bytes name-too-long/568.js 414 bytes name-too-long/cccccccccccccccccccccccccccccc.js 6.52 KiB
+  Entrypoint aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa 15 KiB = name-too-long/282.js 412 bytes name-too-long/954.js 412 bytes name-too-long/767.js 412 bytes name-too-long/390.js 412 bytes name-too-long/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.js 13.4 KiB
+  Entrypoint bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb 8.13 KiB = name-too-long/282.js 412 bytes name-too-long/954.js 412 bytes name-too-long/767.js 412 bytes name-too-long/568.js 412 bytes name-too-long/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.js 6.52 KiB
+  Entrypoint cccccccccccccccccccccccccccccc 8.13 KiB = name-too-long/282.js 412 bytes name-too-long/769.js 412 bytes name-too-long/767.js 412 bytes name-too-long/568.js 412 bytes name-too-long/cccccccccccccccccccccccccccccc.js 6.52 KiB
   chunk (runtime: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, main) name-too-long/async-g.js (async-g) 45 bytes <{282}> <{390}> <{751}> <{767}> <{794}> <{954}> ={568}= [rendered]
     > ./g ./a.js 6:0-47
     ./g.js 45 bytes [built] [code generated]
@@ -3844,8 +3844,8 @@ name-too-long:
 custom-chunks-filter:
   Entrypoint main 11.5 KiB = custom-chunks-filter/main.js
   Entrypoint a 12.6 KiB = custom-chunks-filter/a.js
-  Entrypoint b 8.14 KiB = custom-chunks-filter/282.js 414 bytes custom-chunks-filter/954.js 414 bytes custom-chunks-filter/568.js 414 bytes custom-chunks-filter/767.js 414 bytes custom-chunks-filter/b.js 6.52 KiB
-  Entrypoint c 8.14 KiB = custom-chunks-filter/282.js 414 bytes custom-chunks-filter/769.js 414 bytes custom-chunks-filter/568.js 414 bytes custom-chunks-filter/767.js 414 bytes custom-chunks-filter/c.js 6.52 KiB
+  Entrypoint b 8.13 KiB = custom-chunks-filter/282.js 412 bytes custom-chunks-filter/954.js 412 bytes custom-chunks-filter/568.js 412 bytes custom-chunks-filter/767.js 412 bytes custom-chunks-filter/b.js 6.52 KiB
+  Entrypoint c 8.13 KiB = custom-chunks-filter/282.js 412 bytes custom-chunks-filter/769.js 412 bytes custom-chunks-filter/568.js 412 bytes custom-chunks-filter/767.js 412 bytes custom-chunks-filter/c.js 6.52 KiB
   chunk (runtime: b) custom-chunks-filter/b.js (b) 116 bytes (javascript) 2.76 KiB (runtime) ={282}= ={568}= ={767}= ={954}= [entry] [rendered]
     > ./b b
     runtime modules 2.76 KiB 4 modules
@@ -3909,9 +3909,9 @@ custom-chunks-filter:
 
 custom-chunks-filter-in-cache-groups:
   Entrypoint main 11.3 KiB = custom-chunks-filter-in-cache-groups/main.js
-  Entrypoint a 14.6 KiB = custom-chunks-filter-in-cache-groups/176.js 864 bytes custom-chunks-filter-in-cache-groups/a.js 13.8 KiB
-  Entrypoint b 8.45 KiB = custom-chunks-filter-in-cache-groups/vendors.js 1.05 KiB custom-chunks-filter-in-cache-groups/b.js 7.4 KiB
-  Entrypoint c 8.45 KiB = custom-chunks-filter-in-cache-groups/vendors.js 1.05 KiB custom-chunks-filter-in-cache-groups/c.js 7.4 KiB
+  Entrypoint a 14.6 KiB = custom-chunks-filter-in-cache-groups/176.js 860 bytes custom-chunks-filter-in-cache-groups/a.js 13.7 KiB
+  Entrypoint b 8.44 KiB = custom-chunks-filter-in-cache-groups/vendors.js 1.05 KiB custom-chunks-filter-in-cache-groups/b.js 7.39 KiB
+  Entrypoint c 8.44 KiB = custom-chunks-filter-in-cache-groups/vendors.js 1.05 KiB custom-chunks-filter-in-cache-groups/c.js 7.39 KiB
   chunk (runtime: b) custom-chunks-filter-in-cache-groups/b.js (b) 156 bytes (javascript) 2.76 KiB (runtime) ={216}= [entry] [rendered]
     > ./b b
     > x b
@@ -4109,7 +4109,7 @@ default (webpack x.x.x) compiled successfully"
 `;
 
 exports[`StatsTestCases should print correct stats for split-chunks-issue-6696 1`] = `
-"Entrypoint main 13.4 KiB = vendors.js 414 bytes main.js 13 KiB
+"Entrypoint main 13.4 KiB = vendors.js 412 bytes main.js 13 KiB
 chunk (runtime: main) main.js (main) 134 bytes (javascript) 7.57 KiB (runtime) ={216}= >{334}< >{794}< [entry] [rendered]
   > ./ main
   runtime modules 7.57 KiB 10 modules
@@ -4129,9 +4129,9 @@ default (webpack x.x.x) compiled successfully"
 `;
 
 exports[`StatsTestCases should print correct stats for split-chunks-issue-7401 1`] = `
-"Entrypoint a 6.42 KiB = 282.js 414 bytes a.js 6.02 KiB
+"Entrypoint a 6.42 KiB = 282.js 412 bytes a.js 6.02 KiB
 Entrypoint b 10.9 KiB = b.js
-Chunk Group c 797 bytes = 282.js 414 bytes c.js 383 bytes
+Chunk Group c 795 bytes = 282.js 412 bytes c.js 383 bytes
 chunk (runtime: b) b.js (b) 43 bytes (javascript) 6.61 KiB (runtime) >{282}< >{459}< [entry] [rendered]
   > ./b b
   runtime modules 6.61 KiB 9 modules
@@ -4647,11 +4647,11 @@ exports[`StatsTestCases should print correct stats for split-chunks-runtime-spec
 "used-exports:
   asset used-exports-c.js 6.04 KiB [emitted] (name: c)
   asset used-exports-b.js 6.03 KiB [emitted] (name: b)
-  asset used-exports-332.js 424 bytes [emitted]
+  asset used-exports-332.js 422 bytes [emitted]
   asset used-exports-a.js 257 bytes [emitted] (name: a)
   Entrypoint a 257 bytes = used-exports-a.js
-  Entrypoint b 6.44 KiB = used-exports-332.js 424 bytes used-exports-b.js 6.03 KiB
-  Entrypoint c 6.45 KiB = used-exports-332.js 424 bytes used-exports-c.js 6.04 KiB
+  Entrypoint b 6.44 KiB = used-exports-332.js 422 bytes used-exports-b.js 6.03 KiB
+  Entrypoint c 6.45 KiB = used-exports-332.js 422 bytes used-exports-c.js 6.04 KiB
   chunk (runtime: b) used-exports-b.js (b) 54 bytes (javascript) 2.75 KiB (runtime) [entry] [rendered]
     runtime modules 2.75 KiB 4 modules
     ./b.js 54 bytes [built] [code generated]
@@ -4668,10 +4668,10 @@ no-used-exports:
   asset no-used-exports-c.js 6.04 KiB [emitted] (name: c)
   asset no-used-exports-a.js 6.03 KiB [emitted] (name: a)
   asset no-used-exports-b.js 6.03 KiB [emitted] (name: b)
-  asset no-used-exports-332.js 447 bytes [emitted]
-  Entrypoint a 6.47 KiB = no-used-exports-332.js 447 bytes no-used-exports-a.js 6.03 KiB
-  Entrypoint b 6.47 KiB = no-used-exports-332.js 447 bytes no-used-exports-b.js 6.03 KiB
-  Entrypoint c 6.47 KiB = no-used-exports-332.js 447 bytes no-used-exports-c.js 6.04 KiB
+  asset no-used-exports-332.js 443 bytes [emitted]
+  Entrypoint a 6.46 KiB = no-used-exports-332.js 443 bytes no-used-exports-a.js 6.03 KiB
+  Entrypoint b 6.46 KiB = no-used-exports-332.js 443 bytes no-used-exports-b.js 6.03 KiB
+  Entrypoint c 6.47 KiB = no-used-exports-332.js 443 bytes no-used-exports-c.js 6.04 KiB
   chunk (runtime: b) no-used-exports-b.js (b) 54 bytes (javascript) 2.75 KiB (runtime) [entry] [rendered]
     runtime modules 2.75 KiB 4 modules
     ./b.js 54 bytes [built] [code generated]
@@ -4689,10 +4689,10 @@ global:
   asset global-c.js 6.04 KiB [emitted] (name: c)
   asset global-a.js 6.03 KiB [emitted] (name: a)
   asset global-b.js 6.03 KiB [emitted] (name: b)
-  asset global-332.js 447 bytes [emitted]
-  Entrypoint a 6.47 KiB = global-332.js 447 bytes global-a.js 6.03 KiB
-  Entrypoint b 6.47 KiB = global-332.js 447 bytes global-b.js 6.03 KiB
-  Entrypoint c 6.47 KiB = global-332.js 447 bytes global-c.js 6.04 KiB
+  asset global-332.js 443 bytes [emitted]
+  Entrypoint a 6.46 KiB = global-332.js 443 bytes global-a.js 6.03 KiB
+  Entrypoint b 6.46 KiB = global-332.js 443 bytes global-b.js 6.03 KiB
+  Entrypoint c 6.47 KiB = global-332.js 443 bytes global-c.js 6.04 KiB
   chunk (runtime: b) global-b.js (b) 54 bytes (javascript) 2.75 KiB (runtime) [entry] [rendered]
     runtime modules 2.75 KiB 4 modules
     ./b.js 54 bytes [built] [code generated]
@@ -4708,7 +4708,7 @@ global:
 `;
 
 exports[`StatsTestCases should print correct stats for tree-shaking 1`] = `
-"asset bundle.js 6.89 KiB [emitted] (name: main)
+"asset bundle.js 6.88 KiB [emitted] (name: main)
 runtime modules 663 bytes 3 modules
 orphan modules 14 bytes [orphan] 1 module
 cacheable modules 782 bytes
@@ -4760,9 +4760,9 @@ webpack x.x.x compiled with 1 warning in X ms"
 exports[`StatsTestCases should print correct stats for wasm-explorer-examples-sync 1`] = `
 "assets by path *.js 21.7 KiB
   asset bundle.js 16.2 KiB [emitted] (name: main)
-  asset 325.bundle.js 3.9 KiB [emitted]
+  asset 325.bundle.js 3.89 KiB [emitted]
   asset 795.bundle.js 557 bytes [emitted]
-  asset 526.bundle.js 366 bytes [emitted] (id hint: vendors)
+  asset 526.bundle.js 364 bytes [emitted] (id hint: vendors)
   asset 189.bundle.js 243 bytes [emitted]
   asset 517.bundle.js 243 bytes [emitted]
   asset 20.bundle.js 241 bytes [emitted]
diff --git a/test/configCases/code-generation/import-export-format/cjs-module.js b/test/configCases/code-generation/import-export-format/cjs-module.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/import-export-format/cjs-module.js
@@ -0,0 +1,3 @@
+const foo = 42;
+
+module.exports = { foo };
diff --git a/test/configCases/code-generation/import-export-format/harmony-module-2.js b/test/configCases/code-generation/import-export-format/harmony-module-2.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/import-export-format/harmony-module-2.js
@@ -0,0 +1,5 @@
+export const baz = 11;
+
+import { mod3 } from "./index";
+console.log(mod3.apple);
+
diff --git a/test/configCases/code-generation/import-export-format/harmony-module-3.js b/test/configCases/code-generation/import-export-format/harmony-module-3.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/import-export-format/harmony-module-3.js
@@ -0,0 +1 @@
+export var apple = 45;
diff --git a/test/configCases/code-generation/import-export-format/harmony-module.js b/test/configCases/code-generation/import-export-format/harmony-module.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/import-export-format/harmony-module.js
@@ -0,0 +1,5 @@
+export const bar = 42;
+
+const def = -12;
+export default def;
+
diff --git a/test/configCases/code-generation/import-export-format/index.js b/test/configCases/code-generation/import-export-format/index.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/import-export-format/index.js
@@ -0,0 +1,44 @@
+import { foo as cjsexport_harmonyimport } from "./cjs-module";
+import theDefault, { bar as harmonyexport_harmonyimport } from "./harmony-module";
+const { harmonyexport_cjsimport } = require("./harmony-module").bar;
+import { baz as harmonyexport_harmonyimport_2 } from "./harmony-module-2";
+
+import * as mod3 from "./harmony-module-3";
+export { mod3 };
+
+const { expectSourceToContain, expectSourceToMatch } = require("../../../helpers/expectSource");
+const regexEscape = require("../../../helpers/regexEscape.js");
+
+// It's important to use propertyName when generating object members to ensure that the exported property name
+// uses the same accessor syntax (quotes vs. dot notatation) as the imported property name on the other end
+// (which needs to use propertyAccess).  Else, minifiers such as Closure Compiler will not be able to minify correctly.
+it("should use the same accessor syntax for import and export", function() {
+
+	var fs = require("fs");
+	var source = fs.readFileSync(__filename, "utf-8").toString();
+
+	// Reference these imports to generate uses in the source.
+
+	cjsexport_harmonyimport;
+	harmonyexport_harmonyimport;
+	harmonyexport_cjsimport;
+	harmonyexport_harmonyimport_2;
+	theDefault;
+
+	/*********** DO NOT MATCH BELOW THIS LINE ***********/
+
+	// Note that there are no quotes around the "a" and "b" properties in the following lines.
+
+	// Checking harmonyexportinitfragment.js formation of standard export fragment
+	expectSourceToContain(source, "/* harmony export */   a: () => (/* binding */ bar)");
+
+	// Checking formation of imports
+	expectSourceToContain(source, "harmony_module/* bar */.a;");
+	expectSourceToMatch(source, `${regexEscape("const { harmonyexport_cjsimport } = (__webpack_require__(/*! ./harmony-module */ ")}\\d+${regexEscape(")/* .bar */ .a);")}`);
+
+	// Checking concatenatedmodule.js formation of exports
+	expectSourceToContain(source, "a: () => (/* reexport */ harmony_module_3_namespaceObject)");
+
+	// Checking concatenatedmodule.js formation of namespace objects
+	expectSourceToContain(source, "a: () => (apple)");
+});
diff --git a/test/configCases/code-generation/import-export-format/webpack.config.js b/test/configCases/code-generation/import-export-format/webpack.config.js
new file mode 100644
--- /dev/null
+++ b/test/configCases/code-generation/import-export-format/webpack.config.js
@@ -0,0 +1,14 @@
+/** @type {import("../../../../").Configuration} */
+module.exports = {
+	node: {
+		__dirname: false,
+		__filename: false
+	},
+	optimization: {
+		concatenateModules: true,
+		usedExports: true,
+		providedExports: true,
+		minimize: false,
+		mangleExports: "size"
+	}
+};
diff --git a/test/configCases/output-module/reuse-webpack-esm-library/lib.js b/test/configCases/output-module/reuse-webpack-esm-library/lib.js
--- a/test/configCases/output-module/reuse-webpack-esm-library/lib.js
+++ b/test/configCases/output-module/reuse-webpack-esm-library/lib.js
@@ -77,8 +77,8 @@ var __webpack_exports__ = {};
 		\***************************/
 	__webpack_require__.r(__webpack_exports__);
 	/* harmony export */ __webpack_require__.d(__webpack_exports__, {
-		/* harmony export */   "useCall": () => (/* binding */ useCall),
-		/* harmony export */   "withCallManager": () => (/* binding */ withCallManager)
+		/* harmony export */   useCall: () => (/* binding */ useCall),
+		/* harmony export */   withCallManager: () => (/* binding */ withCallManager)
 		/* harmony export */ });
 	/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! react */ "react");
 
diff --git a/test/helpers/expectSource.js b/test/helpers/expectSource.js
new file mode 100644
--- /dev/null
+++ b/test/helpers/expectSource.js
@@ -0,0 +1,17 @@
+var regexEscape = require("./regexEscape.js");
+
+// These expect* methods are necessary because 'source' contains the code for this test file, which will always contain the string
+// being tested for, so we have to use the "DO NOT MATCH BELOW..." technique to exclude the actual testing code from the test.
+// Place your jest 'expect' calls below a line containing the DO NOT MATCH BELOW... string constructed below.  See other tests for examples.
+
+// Break up the match string so we don't match it in these expect* functions either.
+const doNotMatch = ["DO", "NOT", "MATCH", "BELOW", "THIS", "LINE"].join(" ");
+
+function expectSourceToContain(source, str) {
+	expect(source).toMatch(new RegExp(regexEscape(str) + ".*" + doNotMatch, "s"));
+}
+function expectSourceToMatch(source, regexStr) {
+	expect(source).toMatch(new RegExp(regexStr + ".*" + doNotMatch, "s"));
+}
+
+module.exports = { expectSourceToContain, expectSourceToMatch };
diff --git a/test/helpers/regexEscape.js b/test/helpers/regexEscape.js
new file mode 100644
--- /dev/null
+++ b/test/helpers/regexEscape.js
@@ -0,0 +1,3 @@
+module.exports = function regexEscape(string) {
+	return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
+};
diff --git a/test/propertyAccess.unittest.js b/test/propertyAccess.unittest.js
new file mode 100644
--- /dev/null
+++ b/test/propertyAccess.unittest.js
@@ -0,0 +1,26 @@
+const propertyAccess = require("../lib/util/propertyAccess");
+
+describe("propertyAccess", () => {
+	it("brackets but does not quote numbers", () => {
+		expect(propertyAccess(["12"])).toBe("[12]");
+	});
+
+	it("brackets and quotes special cases", () => {
+		expect(propertyAccess(["class"])).toBe('["class"]');
+		expect(propertyAccess(["white space"])).toBe('["white space"]');
+		expect(propertyAccess(["3cc"])).toBe('["3cc"]');
+	});
+
+	it("uses dot notation on all other cases", () => {
+		expect(propertyAccess(["a"])).toBe(".a");
+		expect(propertyAccess(["_xyz"])).toBe("._xyz");
+		expect(propertyAccess(["cc3"])).toBe(".cc3");
+	});
+
+	it("handles multiple levels", () => {
+		expect(propertyAccess(["a", "b", "c"])).toBe(".a.b.c");
+		expect(propertyAccess(["null", "await", "if"])).toBe(
+			'["null"]["await"]["if"]'
+		);
+	});
+});
diff --git a/test/propertyName.unittest.js b/test/propertyName.unittest.js
new file mode 100644
--- /dev/null
+++ b/test/propertyName.unittest.js
@@ -0,0 +1,15 @@
+const { propertyName } = require("../lib/util/propertyName");
+
+describe("propertyName", () => {
+	it("quotes special cases", () => {
+		expect(propertyName("class")).toBe('"class"');
+		expect(propertyName("white space")).toBe('"white space"');
+		expect(propertyName("3cc")).toBe('"3cc"');
+	});
+
+	it("passes non-special cases through", () => {
+		expect(propertyName("a")).toBe("a");
+		expect(propertyName("_xyz")).toBe("_xyz");
+		expect(propertyName("cc3")).toBe("cc3");
+	});
+});