diff --git a/.changeset/shy-terms-study.md b/.changeset/shy-terms-study.md new file mode 100644 index 0000000..3de956d --- /dev/null +++ b/.changeset/shy-terms-study.md @@ -0,0 +1,9 @@ +--- +"webidl-dts-gen": minor +--- + +feat: improve output types for emscripten enums + +The emscripten webidl binder exposes enum values using enum member names. e.g. `Module.MemberName`, not `Module.EnumName.MemberName`. The output types now reflect this. + +Also, types for the emscripten enum wrapper functions are now exposed, e.g. `_emscripten_enum_EnumName_MemberName` diff --git a/packages/webidl-dts-gen/src/convert-idl.ts b/packages/webidl-dts-gen/src/convert-idl.ts index 943b5da..b8bdd26 100644 --- a/packages/webidl-dts-gen/src/convert-idl.ts +++ b/packages/webidl-dts-gen/src/convert-idl.ts @@ -37,6 +37,8 @@ const baseTypeConversionMap = new Map([ export function convertIDL(rootTypes: webidl2.IDLRootType[], options: Options = {}): ts.Statement[] { const nodes: ts.Statement[] = [] + const emscriptenEnumMembers: Set = new Set() + for (const rootType of rootTypes) { switch (rootType.type) { case 'interface': @@ -69,7 +71,7 @@ export function convertIDL(rootTypes: webidl2.IDLRootType[], options: Options = nodes.push(convertInterfaceIncludes(rootType)) break case 'enum': - nodes.push(convertEnum(rootType)) + nodes.push(...convertEnum(rootType, options, emscriptenEnumMembers)) break case 'callback': nodes.push(convertCallback(rootType)) @@ -432,13 +434,70 @@ function convertType(idl: webidl2.IDLTypeDescription): ts.TypeNode { return ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) } -function convertEnum(idl: webidl2.EnumType) { - return ts.factory.createTypeAliasDeclaration( +function convertEnum(idl: webidl2.EnumType, options: Options, emscriptenEnumMembers: Set) { + if (!options.emscripten) { + return [ + ts.factory.createTypeAliasDeclaration( + undefined, + ts.factory.createIdentifier(idl.name), + undefined, + ts.factory.createUnionTypeNode(idl.values.map((it) => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(it.value)))), + ), + ] + } + + const memberNames = idl.values.map((it) => { + // Strip the namespace from the member name if present, e.g. `EnumNamespace::` in "EnumNamespace::e_namespace_val" + // see: https://emscripten.org/docs/porting/connecting_cpp_and_javascript/WebIDL-Binder.html#enums + return it.value.replace(/.*::/, '') + }) + + // emscripten enums are exposed on the module their names, e.g. 'Module.MemberName' + // create a variable declaration for each enum member + const enumVariableDeclarations = memberNames + .map((member) => { + if (emscriptenEnumMembers.has(member)) { + console.warn( + `Duplicate enum member name: '${member}'. Omitting duplicate from types. Enums in emscripten are exposed on the module their names, e.g. 'Module.MemberName', not 'Module.Enum.MemberName'.`, + ) + return undefined + } + + emscriptenEnumMembers.add(member) + + const variableDeclaration = ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(member), + undefined, + ts.factory.createTypeReferenceNode('unknown', undefined), + ) + + return ts.factory.createVariableStatement( + undefined, + ts.factory.createVariableDeclarationList([variableDeclaration], ts.NodeFlags.Const), + ) + }) + .filter(Boolean) + + const enumVariableDeclarationsUnionType = ts.factory.createTypeAliasDeclaration( undefined, ts.factory.createIdentifier(idl.name), undefined, - ts.factory.createUnionTypeNode(idl.values.map((it) => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(it.value)))), + ts.factory.createUnionTypeNode(memberNames.map((it) => ts.factory.createTypeReferenceNode(`typeof ${it}`, undefined))), ) + + const emscriptenInternalWrapperFunctions = memberNames.map((member) => { + return ts.factory.createFunctionDeclaration( + undefined, + undefined, + ts.factory.createIdentifier(`_emscripten_enum_${idl.name}_${member}`), + undefined, + [], + ts.factory.createTypeReferenceNode(idl.name, undefined), + undefined, + ) + }) + + return [...enumVariableDeclarations, enumVariableDeclarationsUnionType, ...emscriptenInternalWrapperFunctions] } function convertCallback(idl: webidl2.CallbackType) { diff --git a/packages/webidl-dts-gen/tst/index.spec.ts b/packages/webidl-dts-gen/tst/index.spec.ts index daea854..e0503aa 100644 --- a/packages/webidl-dts-gen/tst/index.spec.ts +++ b/packages/webidl-dts-gen/tst/index.spec.ts @@ -107,6 +107,140 @@ describe('convert', () => { ) }) + describe('enums', () => { + it('converts enums to union types', async () => { + const idl = multiLine( + 'enum Foo {', // + ' "bar",', // + ' "baz"', // + '};', // + ) + + const ts = await convert(idl) + + expect(ts).toBe( + multiLine( + 'type Foo = "bar" | "baz";', // + ), + ) + }) + + describe('emscripten enabled', () => { + it('supports enums', async () => { + const idl = multiLine( + 'enum Foo {', // + ' "bar",', // + ' "baz"', // + '};', // + ) + + const ts = await convert(idl, { emscripten: true }) + + expect(ts).toBe( + multiLine( + 'declare function Module(target?: T): Promise;', // + 'declare module Module {', // + ' function destroy(obj: any): void;', // + ' function _malloc(size: number): number;', // + ' function _free(ptr: number): void;', // + ' const HEAP8: Int8Array;', // + ' const HEAP16: Int16Array;', // + ' const HEAP32: Int32Array;', // + ' const HEAPU8: Uint8Array;', // + ' const HEAPU16: Uint16Array;', // + ' const HEAPU32: Uint32Array;', // + ' const HEAPF32: Float32Array;', // + ' const HEAPF64: Float64Array;', // + ' const bar: unknown;', // + ' const baz: unknown;', // + ' type Foo = typeof bar | typeof baz;', // + ' function _emscripten_enum_Foo_bar(): Foo;', // + ' function _emscripten_enum_Foo_baz(): Foo;', // + '}', // + ), + ) + }) + + it('supports enums declared in namespaces', async () => { + const idl = multiLine( + 'enum Foo {', // + ' "namespace::bar",', // + ' "namespace::baz"', // + '};', // + ) + + const ts = await convert(idl, { emscripten: true }) + + expect(ts).toBe( + multiLine( + 'declare function Module(target?: T): Promise;', // + 'declare module Module {', // + ' function destroy(obj: any): void;', // + ' function _malloc(size: number): number;', // + ' function _free(ptr: number): void;', // + ' const HEAP8: Int8Array;', // + ' const HEAP16: Int16Array;', // + ' const HEAP32: Int32Array;', // + ' const HEAPU8: Uint8Array;', // + ' const HEAPU16: Uint16Array;', // + ' const HEAPU32: Uint32Array;', // + ' const HEAPF32: Float32Array;', // + ' const HEAPF64: Float64Array;', // + ' const bar: unknown;', // + ' const baz: unknown;', // + ' type Foo = typeof bar | typeof baz;', // + ' function _emscripten_enum_Foo_bar(): Foo;', // + ' function _emscripten_enum_Foo_baz(): Foo;', // + '}', // + ), + ) + }) + + it('omits duplicate enum member names from the generated types', async () => { + const idl = multiLine( + 'enum Foo {', // + ' "namespace::bar",', // + ' "namespace::baz"', // + '};', // + 'enum Bar {', // + ' "namespace::bar",', // + ' "namespace::baz"', // + '};', // + ) + + const ts = await convert(idl, { emscripten: true }) + + expect(ts).toBe( + multiLine( + 'declare function Module(target?: T): Promise;', // + 'declare module Module {', // + ' function destroy(obj: any): void;', // + ' function _malloc(size: number): number;', // + ' function _free(ptr: number): void;', // + ' const HEAP8: Int8Array;', // + ' const HEAP16: Int16Array;', // + ' const HEAP32: Int32Array;', // + ' const HEAPU8: Uint8Array;', // + ' const HEAPU16: Uint16Array;', // + ' const HEAPU32: Uint32Array;', // + ' const HEAPF32: Float32Array;', // + ' const HEAPF64: Float64Array;', // + ' const bar: unknown;', // + ' const baz: unknown;', // + ' type Foo = typeof bar | typeof baz;', // + ' function _emscripten_enum_Foo_bar(): Foo;', // + ' function _emscripten_enum_Foo_baz(): Foo;', // + ' type Bar = typeof bar | typeof baz;', // + ' function _emscripten_enum_Bar_bar(): Bar;', // + ' function _emscripten_enum_Bar_baz(): Bar;', // + '}', // + ), + ) + }) + }) + + }) + describe('emscripten', () => { it('supports unsigned integer arrays', async () => { const idl = multiLine(