Skip to content

Commit

Permalink
feat(pluginutils): add includeArbitraryNames: true to dataToEsm (#…
Browse files Browse the repository at this point in the history
…1635)

* feat(pluginutils): add `namedExports: 'include-arbitrary-name'` to `dataToEsm`

* refactor(pluginutils):  change to includeArbitraryNames

* docs: add options to dataToEsm section

* docs: add option description to new option

---------

Co-authored-by: Andrew Powell <shellscape@users.noreply.github.com>
  • Loading branch information
sapphi-red and shellscape committed Nov 28, 2023
1 parent 7af1e21 commit 3c53a29
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 3 deletions.
11 changes: 9 additions & 2 deletions packages/pluginutils/README.md
Expand Up @@ -143,7 +143,7 @@ export default function myPlugin(options = {}) {

Transforms objects into tree-shakable ES Module imports.

Parameters: `(data: Object)`<br>
Parameters: `(data: Object, options: DataToEsmOptions)`<br>
Returns: `String`

#### `data`
Expand All @@ -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
Expand All @@ -167,7 +173,8 @@ const esModuleSource = dataToEsm(
indent: '\t',
preferConst: true,
objectShorthand: true,
namedExports: true
namedExports: true,
includeArbitraryNames: false
}
);
/*
Expand Down
40 changes: 39 additions & 1 deletion packages/pluginutils/src/dataToEsm.ts
Expand Up @@ -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 ? '' : ' ';
Expand All @@ -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);
Expand All @@ -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 };
17 changes: 17 additions & 0 deletions packages/pluginutils/test/dataToEsm.ts
Expand Up @@ -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"};'
);
});
6 changes: 6 additions & 0 deletions packages/pluginutils/types/index.d.ts
Expand Up @@ -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;
Expand Down

0 comments on commit 3c53a29

Please sign in to comment.