Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pluginutils): add includeArbitraryNames: true to dataToEsm #1635

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions packages/pluginutils/README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading