From d14a92f2deb23af839610c43c535c4abd13de7b8 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Wed, 22 Nov 2023 20:56:42 +0900 Subject: [PATCH 1/4] feat(pluginutils): add `namedExports: 'include-arbitrary-name'` to `dataToEsm` --- packages/pluginutils/src/dataToEsm.ts | 40 +++++++++++++++++++++++++- packages/pluginutils/test/dataToEsm.ts | 17 +++++++++++ packages/pluginutils/types/index.d.ts | 2 +- 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/pluginutils/src/dataToEsm.ts b/packages/pluginutils/src/dataToEsm.ts index d51d59e1a..326741e9d 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.namedExports === 'include-arbitrary-name' && 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..dff99c856 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: 'include-arbitrary-name' } + ), + '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: 'include-arbitrary-name', 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..e45933e5a 100755 --- a/packages/pluginutils/types/index.d.ts +++ b/packages/pluginutils/types/index.d.ts @@ -11,7 +11,7 @@ export interface AttachedScope { export interface DataToEsmOptions { compact?: boolean; indent?: string; - namedExports?: boolean; + namedExports?: boolean | 'include-arbitrary-name'; objectShorthand?: boolean; preferConst?: boolean; } From d732d59faf4f2ad4449f265387cc1249e0867960 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Sun, 26 Nov 2023 20:38:52 +0900 Subject: [PATCH 2/4] refactor(pluginutils): change to includeArbitraryNames --- packages/pluginutils/README.md | 3 ++- packages/pluginutils/src/dataToEsm.ts | 2 +- packages/pluginutils/test/dataToEsm.ts | 4 ++-- packages/pluginutils/types/index.d.ts | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/pluginutils/README.md b/packages/pluginutils/README.md index e705cb912..24b392148 100755 --- a/packages/pluginutils/README.md +++ b/packages/pluginutils/README.md @@ -167,7 +167,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 326741e9d..312017220 100755 --- a/packages/pluginutils/src/dataToEsm.ts +++ b/packages/pluginutils/src/dataToEsm.ts @@ -115,7 +115,7 @@ const dataToEsm: DataToEsm = function dataToEsm(data, options = {}) { defaultExportRows.push( `${stringify(key)}:${_}${serialize(value, options.compact ? null : t, '')}` ); - if (options.namedExports === 'include-arbitrary-name' && isWellFormedString(key)) { + if (options.includeArbitraryNames && isWellFormedString(key)) { const variableName = `${arbitraryNamePrefix}${arbitraryNameExportRows.length}`; namedExportCode += `${declarationType} ${variableName}${_}=${_}${serialize( value, diff --git a/packages/pluginutils/test/dataToEsm.ts b/packages/pluginutils/test/dataToEsm.ts index dff99c856..d32fe3e24 100755 --- a/packages/pluginutils/test/dataToEsm.ts +++ b/packages/pluginutils/test/dataToEsm.ts @@ -115,14 +115,14 @@ test('support arbitrary module namespace identifier names', (t) => { t.is( dataToEsm( { foo: 'foo', 'foo.bar': 'foo.bar', '\udfff': 'non wellformed' }, - { namedExports: 'include-arbitrary-name' } + { 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: 'include-arbitrary-name', compact: true } + { 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 e45933e5a..15a06f3e4 100755 --- a/packages/pluginutils/types/index.d.ts +++ b/packages/pluginutils/types/index.d.ts @@ -11,7 +11,8 @@ export interface AttachedScope { export interface DataToEsmOptions { compact?: boolean; indent?: string; - namedExports?: boolean | 'include-arbitrary-name'; + namedExports?: boolean; + includeArbitraryNames?: boolean; objectShorthand?: boolean; preferConst?: boolean; } From 16d6804c0991628a6a47a6795142440972d7b040 Mon Sep 17 00:00:00 2001 From: Andrew Powell Date: Tue, 28 Nov 2023 09:45:02 -0500 Subject: [PATCH 3/4] docs: add options to dataToEsm section --- packages/pluginutils/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/pluginutils/README.md b/packages/pluginutils/README.md index 24b392148..8f8219802 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 From 1239d7db94f0d1787798ecaab6254ea5c163fc71 Mon Sep 17 00:00:00 2001 From: Andrew Powell Date: Tue, 28 Nov 2023 09:47:35 -0500 Subject: [PATCH 4/4] docs: add option description to new option --- packages/pluginutils/types/index.d.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/pluginutils/types/index.d.ts b/packages/pluginutils/types/index.d.ts index 15a06f3e4..4bdf3a2a6 100755 --- a/packages/pluginutils/types/index.d.ts +++ b/packages/pluginutils/types/index.d.ts @@ -10,9 +10,14 @@ 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; - includeArbitraryNames?: boolean; objectShorthand?: boolean; preferConst?: boolean; }