Skip to content

Commit

Permalink
feat: improve output types for emscripten enums
Browse files Browse the repository at this point in the history
  • Loading branch information
isaac-mason committed Apr 15, 2023
1 parent f5d6dea commit afc8c20
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 4 deletions.
9 changes: 9 additions & 0 deletions .changeset/shy-terms-study.md
Original file line number Diff line number Diff line change
@@ -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`
67 changes: 63 additions & 4 deletions packages/webidl-dts-gen/src/convert-idl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ const baseTypeConversionMap = new Map<string, string>([
export function convertIDL(rootTypes: webidl2.IDLRootType[], options: Options = {}): ts.Statement[] {
const nodes: ts.Statement[] = []

const emscriptenEnumMembers: Set<string> = new Set()

for (const rootType of rootTypes) {
switch (rootType.type) {
case 'interface':
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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<string>) {
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) {
Expand Down
134 changes: 134 additions & 0 deletions packages/webidl-dts-gen/tst/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(target?: T): Promise<T & typeof Module>;', //
'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<T>(target?: T): Promise<T & typeof Module>;', //
'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<T>(target?: T): Promise<T & typeof Module>;', //
'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(
Expand Down

0 comments on commit afc8c20

Please sign in to comment.