diff --git a/packages/pluginutils/README.md b/packages/pluginutils/README.md
index 8d87f5dba..368bce87d 100755
--- a/packages/pluginutils/README.md
+++ b/packages/pluginutils/README.md
@@ -143,7 +143,7 @@ export default function myPlugin(options = {}) {
Transforms objects into tree-shakable ES Module imports.
-Parameters: `(data: Object)`
+Parameters: `(data: Object, options: DataToEsmOptions)`
Returns: `String`
#### `data`
@@ -152,6 +152,12 @@ Type: `Object`
An object to transform into an ES module.
+#### `options`
+
+Type: `DataToEsmOptions`
+
+_Note: Please see the TypeScript definition for complete documentation of these options_
+
#### Usage
```js
@@ -167,7 +173,8 @@ const esModuleSource = dataToEsm(
indent: '\t',
preferConst: true,
objectShorthand: true,
- namedExports: true
+ namedExports: true,
+ includeArbitraryNames: false
}
);
/*
diff --git a/packages/pluginutils/src/dataToEsm.ts b/packages/pluginutils/src/dataToEsm.ts
index d51d59e1a..312017220 100755
--- a/packages/pluginutils/src/dataToEsm.ts
+++ b/packages/pluginutils/src/dataToEsm.ts
@@ -59,6 +59,17 @@ function serialize(obj: unknown, indent: Indent, baseIndent: string): string {
return stringify(obj);
}
+// isWellFormed exists from Node.js 20
+const hasStringIsWellFormed = 'isWellFormed' in String.prototype;
+
+function isWellFormedString(input: string): boolean {
+ // @ts-expect-error String::isWellFormed exists from ES2024. tsconfig lib is set to ES6
+ if (hasStringIsWellFormed) return input.isWellFormed();
+
+ // https://github.com/tc39/proposal-is-usv-string/blob/main/README.md#algorithm
+ return !/\p{Surrogate}/u.test(input);
+}
+
const dataToEsm: DataToEsm = function dataToEsm(data, options = {}) {
const t = options.compact ? '' : 'indent' in options ? options.indent : '\t';
const _ = options.compact ? '' : ' ';
@@ -78,8 +89,19 @@ const dataToEsm: DataToEsm = function dataToEsm(data, options = {}) {
return `export default${magic}${code};`;
}
+ let maxUnderbarPrefixLength = 0;
+ for (const key of Object.keys(data)) {
+ const underbarPrefixLength = key.match(/^(_+)/)?.[0].length ?? 0;
+ if (underbarPrefixLength > maxUnderbarPrefixLength) {
+ maxUnderbarPrefixLength = underbarPrefixLength;
+ }
+ }
+
+ const arbitraryNamePrefix = `${'_'.repeat(maxUnderbarPrefixLength + 1)}arbitrary`;
+
let namedExportCode = '';
const defaultExportRows = [];
+ const arbitraryNameExportRows: string[] = [];
for (const [key, value] of Object.entries(data)) {
if (key === makeLegalIdentifier(key)) {
if (options.objectShorthand) defaultExportRows.push(key);
@@ -93,11 +115,27 @@ const dataToEsm: DataToEsm = function dataToEsm(data, options = {}) {
defaultExportRows.push(
`${stringify(key)}:${_}${serialize(value, options.compact ? null : t, '')}`
);
+ if (options.includeArbitraryNames && isWellFormedString(key)) {
+ const variableName = `${arbitraryNamePrefix}${arbitraryNameExportRows.length}`;
+ namedExportCode += `${declarationType} ${variableName}${_}=${_}${serialize(
+ value,
+ options.compact ? null : t,
+ ''
+ )};${n}`;
+ arbitraryNameExportRows.push(`${variableName} as ${JSON.stringify(key)}`);
+ }
}
}
- return `${namedExportCode}export default${_}{${n}${t}${defaultExportRows.join(
+
+ const arbitraryExportCode =
+ arbitraryNameExportRows.length > 0
+ ? `export${_}{${n}${t}${arbitraryNameExportRows.join(`,${n}${t}`)}${n}};${n}`
+ : '';
+ const defaultExportCode = `export default${_}{${n}${t}${defaultExportRows.join(
`,${n}${t}`
)}${n}};${n}`;
+
+ return `${namedExportCode}${arbitraryExportCode}${defaultExportCode}`;
};
export { dataToEsm as default };
diff --git a/packages/pluginutils/test/dataToEsm.ts b/packages/pluginutils/test/dataToEsm.ts
index d1d5d9ac5..d32fe3e24 100755
--- a/packages/pluginutils/test/dataToEsm.ts
+++ b/packages/pluginutils/test/dataToEsm.ts
@@ -110,3 +110,20 @@ test('avoid U+2029 U+2029 -0 be ignored by JSON.stringify, and avoid it return n
'export default[-0,"\\u2028\\u2029",undefined,undefined];'
);
});
+
+test('support arbitrary module namespace identifier names', (t) => {
+ t.is(
+ dataToEsm(
+ { foo: 'foo', 'foo.bar': 'foo.bar', '\udfff': 'non wellformed' },
+ { namedExports: true, includeArbitraryNames: true }
+ ),
+ 'export var foo = "foo";\nvar _arbitrary0 = "foo.bar";\nexport {\n\t_arbitrary0 as "foo.bar"\n};\nexport default {\n\tfoo: foo,\n\t"foo.bar": "foo.bar",\n\t"\\udfff": "non wellformed"\n};\n'
+ );
+ t.is(
+ dataToEsm(
+ { foo: 'foo', 'foo.bar': 'foo.bar', '\udfff': 'non wellformed' },
+ { namedExports: true, includeArbitraryNames: true, compact: true }
+ ),
+ 'export var foo="foo";var _arbitrary0="foo.bar";export{_arbitrary0 as "foo.bar"};export default{foo:foo,"foo.bar":"foo.bar","\\udfff":"non wellformed"};'
+ );
+});
diff --git a/packages/pluginutils/types/index.d.ts b/packages/pluginutils/types/index.d.ts
index fb96d6346..4bdf3a2a6 100755
--- a/packages/pluginutils/types/index.d.ts
+++ b/packages/pluginutils/types/index.d.ts
@@ -10,6 +10,12 @@ export interface AttachedScope {
export interface DataToEsmOptions {
compact?: boolean;
+ /**
+ * @desc When this option is set, dataToEsm will generate a named export for keys that
+ * are not a valid identifier, by leveraging the "Arbitrary Module Namespace Identifier
+ * Names" feature. See: https://github.com/tc39/ecma262/pull/2154
+ */
+ includeArbitraryNames?: boolean;
indent?: string;
namedExports?: boolean;
objectShorthand?: boolean;