├── .gitignore ├── .npmignore ├── .vscode ├── settings.json └── launch.json ├── tsconfig.json ├── src ├── types.ts ├── renderers │ ├── renderEnumDeclarations.ts │ ├── renderImportDeclarations.ts │ ├── renderTypeDeclarations │ │ ├── renderUnannType.ts │ │ └── index.ts │ └── util.ts ├── main.ts ├── additional-types.d.ts.template ├── visit.ts ├── mods.ts └── build.ts ├── README.md ├── package.json └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /java_source 2 | /node_modules 3 | /dist 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /java_source 3 | /src 4 | /tsconfig.json 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "resolveJsonModule": true, 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { CstNodeLocation } from 'chevrotain'; 2 | 3 | export type AnyCstNode = { 4 | name: string; 5 | children: any; 6 | location?: CstNodeLocation; 7 | fullName?: string; 8 | }; 9 | 10 | declare module 'chevrotain/lib/chevrotain' { 11 | interface CstNode { 12 | leadingComments?: IToken[]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Launch Program", 8 | "runtimeArgs": [ 9 | "-r", 10 | "ts-node/register" 11 | ], 12 | "args": [ 13 | "${workspaceFolder}/src/build.ts" 14 | ], 15 | "resolveSourceMapLocations": [ 16 | "${workspaceFolder}/src/**" 17 | ], 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /src/renderers/renderEnumDeclarations.ts: -------------------------------------------------------------------------------- 1 | import { AnyCstNode } from '../types'; 2 | import { visit } from '../visit'; 3 | 4 | export function renderEnumDeclarations(node: AnyCstNode) { 5 | const enumDecs = visit(node, 'enumDeclaration'); 6 | 7 | const enums = enumDecs 8 | .map((ed) => { 9 | const name = ed.typeIdentifier?.[0].children.Identifier?.[0].image; 10 | const body = ed.enumBody?.[0].children.enumConstantList?.[0].children.enumConstant 11 | ?.map((c, i) => `${c.children.Identifier?.[0].image} = ${i},`) 12 | .join('\n'); 13 | return `enum ${name} {\n${body}\n}\n`; 14 | }) 15 | .join('\n'); 16 | 17 | return enums; 18 | } 19 | -------------------------------------------------------------------------------- /src/renderers/renderImportDeclarations.ts: -------------------------------------------------------------------------------- 1 | import { CstNode } from 'chevrotain'; 2 | import { ImportDeclarationCstNode } from 'java-parser'; 3 | import { visit } from '../visit'; 4 | import { ignoredImports } from './util'; 5 | 6 | export function renderImportDeclarations(nodes?: ImportDeclarationCstNode[]) { 7 | if (!nodes) return ''; 8 | 9 | const imports = nodes.map((node) => 10 | visit(node as CstNode, 'packageOrTypeName')[0].Identifier.map( 11 | (id) => id.image 12 | ) 13 | ); 14 | return imports 15 | .map((parts) => { 16 | const importPathString = parts.join('.').replace('.function.', '.func.'); 17 | if (ignoredImports.includes(importPathString)) { 18 | return ''; 19 | } 20 | return `import ${parts.slice(-1)[0]} = ${importPathString};\n`; 21 | }) 22 | .join(''); 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typed-bitwig-api 2 | 3 | Typescript types definition file for Bitwig Control Surface API. 4 | 5 | 6 | ## Installation 7 | 8 | ```bash 9 | $ npm install typed-bitwig-api 10 | ``` 11 | 12 | In your `tsconfig.json` for your project add the following to the `types` list: 13 | 14 | ```js 15 | { 16 | ... 17 | "types": [ 18 | ..., 19 | "typed-bitwig-api" 20 | ], 21 | ... 22 | } 23 | ``` 24 | 25 | 26 | ## Usage 27 | 28 | With the above installation complete, the `host` variable should be seen as available globally (along with `load`, `loadAPI`, `println`, `dump`, etc). API interfaces are made available for reference through the globally accessible `API` namespace. So, for example, if you wrote a function that took an instance of the API's `Transport` object, you would reference it as follows: 29 | 30 | ```ts 31 | function foo(transport: API.Transport) { 32 | // ... do something 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typed-bitwig-api", 3 | "version": "25.0.0", 4 | "repository": "joslarson/typed-bitwig-api", 5 | "description": "TypeScript definition file for the Bitwig Studio Control Surface Scripting API.", 6 | "types": "bitwig-api.d.ts", 7 | "scripts": { 8 | "build": "node -r ts-node/register src/main.ts && npm run check", 9 | "check": "tsc --noEmit bitwig-api.d.ts", 10 | "version": "npm run build && git add bitwig-api.d.ts" 11 | }, 12 | "author": "Joseph Larson", 13 | "license": "BSD-3-Clause", 14 | "devDependencies": { 15 | "@types/download": "6.2.4", 16 | "@types/node": "14.14.2", 17 | "@types/prettier": "2.1.5", 18 | "chevrotain": "6.5.0", 19 | "download": "^8.0.0", 20 | "java-parser": "0.8.2", 21 | "prettier": "2.1.2", 22 | "ts-node": "9.0.0", 23 | "typescript": "4.1.2" 24 | }, 25 | "prettier": { 26 | "trailingComma": "es5", 27 | "tabWidth": 2, 28 | "semi": true, 29 | "singleQuote": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/renderers/renderTypeDeclarations/renderUnannType.ts: -------------------------------------------------------------------------------- 1 | import { UnannTypeCstNode } from 'java-parser'; 2 | import { visit } from '../../visit'; 3 | import { getTypeArgs, typeOverrides } from '../util'; 4 | 5 | export const renderUnannType = (node: UnannTypeCstNode) => { 6 | if (node.children.unannPrimitiveType?.[0]) { 7 | // @ts-ignore 8 | const isArrayOfType = node.children.dims; 9 | const primitiveType = node.children.unannPrimitiveType[0].children 10 | .Boolean?.[0] 11 | ? 'boolean' 12 | : 'number'; 13 | return `${primitiveType}${isArrayOfType ? '[]' : ''}`; 14 | } else if (node.children.unannReferenceType?.[0]) { 15 | const name = visit(node, 'unannClassType')?.[0].Identifier?.[0].image; 16 | const typeArgs = getTypeArgs(node); 17 | const isArrayOfType = node.children.unannReferenceType?.[0].children.dims; 18 | 19 | if (!name) throw new Error('unann type name not found'); 20 | 21 | return `${typeOverrides[name] || `${name}${typeArgs}`}${ 22 | isArrayOfType ? '[]' : '' 23 | }`; 24 | } else { 25 | throw new Error('Unhandled unann type'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import download from 'download'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | const spawn = require('child_process').spawnSync; 5 | 6 | import pkg from '../package.json'; 7 | import { build } from './build'; 8 | 9 | const API_VERSION = parseInt(pkg.version.split('.')[0]); 10 | 11 | function downloadApiSource(version: number) { 12 | return download( 13 | `https://maven.bitwig.com/com/bitwig/extension-api/${version}/extension-api-${version}-sources.jar`, 14 | 'java_source', 15 | { extract: true } 16 | ); 17 | } 18 | 19 | async function main(fullBuild = true) { 20 | if (fullBuild) { 21 | console.log('full build...'); 22 | 23 | fs.rmSync('java_source', { recursive: true, force: true }); 24 | 25 | try { 26 | await downloadApiSource(API_VERSION); 27 | fs.rmSync('java_source/com/bitwig/flt', { recursive: true, force: true }); 28 | } catch (e) { 29 | console.error(`Error: Unable to fetch API v${API_VERSION} source files.`); 30 | return; 31 | } 32 | } else { 33 | console.log('minimal build...'); 34 | } 35 | 36 | await build(); 37 | 38 | console.log('done.'); 39 | } 40 | 41 | main(process.argv.slice(2).indexOf('-t') === -1); 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Joseph Larson 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /src/additional-types.d.ts.template: -------------------------------------------------------------------------------- 1 | declare namespace java { 2 | namespace nio { 3 | type ByteBuffer = any; 4 | } 5 | namespace util { 6 | type List = Array; 7 | type UUID = string; 8 | 9 | namespace func { 10 | type BooleanSupplier = { 11 | getAsBoolean(): boolean; 12 | }; 13 | type Consumer = { 14 | accept(t: T): void; 15 | andThen(after: Consumer): Consumer; 16 | }; 17 | type Supplier = { 18 | get(): T; 19 | }; 20 | type DoubleConsumer = { 21 | accept(double: number): void; 22 | andThen(after: DoubleConsumer): DoubleConsumer; 23 | }; 24 | type DoubleSupplier = { 25 | getAsDouble(): number; 26 | }; 27 | type IntConsumer = { 28 | accept(int: number): void; 29 | andThen(after: IntConsumer): IntConsumer; 30 | }; 31 | type IntSupplier = { 32 | getAsInt(): number; 33 | }; 34 | interface Function { 35 | andThen(after: Function): Function; 36 | apply(t: T): R; 37 | compose(before: Function): Function; 38 | } 39 | type IntFunction = { 40 | apply(value: number): R; 41 | }; 42 | } 43 | } 44 | } 45 | 46 | import API = com.bitwig.extension.controller.api; 47 | 48 | declare const host: API.ControllerHost; 49 | declare const loadAPI: typeof host.loadAPI; 50 | declare const load: typeof host.load; 51 | declare const println: typeof host.println; 52 | declare const errorln: typeof host.errorln; 53 | declare function dump(obj: any): void; 54 | -------------------------------------------------------------------------------- /src/visit.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JavaCstVisitorWithDefaults, 3 | BaseJavaCstVisitorWithDefaults, 4 | } from 'java-parser'; 5 | 6 | import { AnyCstNode } from './types'; 7 | 8 | type Last< 9 | T extends (keyof JavaCstVisitorWithDefaults)[] 10 | > = T extends [...infer I, infer L] ? L : never; 11 | 12 | type VisitorKey = Exclude< 13 | keyof JavaCstVisitorWithDefaults, 14 | 'visit' | 'validateVisitor' 15 | >; 16 | 17 | type CtxFromKey = Parameters< 18 | JavaCstVisitorWithDefaults[K] 19 | >[0]; 20 | 21 | const isCstNode = (value: any): value is AnyCstNode => 22 | typeof value === 'object' && 'name' in value && 'children' in value; 23 | 24 | export const visit = ( 25 | node: AnyCstNode, 26 | ...path: T 27 | ): CtxFromKey>[] => { 28 | const result: CtxFromKey[] = []; 29 | 30 | class Collector extends BaseJavaCstVisitorWithDefaults { 31 | [path[0]](ctx: CtxFromKey) { 32 | result.push(ctx); 33 | } 34 | } 35 | 36 | const collector = new Collector(); 37 | 38 | collector.visit(node); 39 | 40 | if (path.length === 1) { 41 | return result; 42 | } 43 | 44 | const cstList: AnyCstNode[] = []; 45 | for (const ctx of result) { 46 | for (const maybeCstList of Object.values(ctx)) { 47 | if (Array.isArray(maybeCstList)) { 48 | cstList.push( 49 | ...maybeCstList.filter((maybeCst: any) => isCstNode(maybeCst)) 50 | ); 51 | } 52 | } 53 | } 54 | 55 | // @ts-ignore 56 | return [].concat(...cstList.map((cst) => visit(cst, ...path.slice(1)))); 57 | }; 58 | -------------------------------------------------------------------------------- /src/mods.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | type Mod = { 5 | file: string; 6 | search: string | RegExp; 7 | replace: string; 8 | }; 9 | 10 | const mods: Mod[] = [ 11 | // ignore invalid method interface method extensions 12 | ...['SoloValue', 'RangedValue', 'IntegerValue', 'StringArrayValue'].map( 13 | (interfaceName) => ({ 14 | file: `com/bitwig/extension/controller/api/${interfaceName}.d.ts`, 15 | search: new RegExp(`^([ ]*)(interface ${interfaceName})`, 'm'), 16 | replace: '$1// @ts-ignore\n$1$2', 17 | }) 18 | ), 19 | { 20 | file: 'com/bitwig/extension/controller/api/ControllerHost.d.ts', 21 | search: 22 | 'scheduleTask(callback: () => void, args: object, delay: number): void;', 23 | replace: 24 | 'scheduleTask(callback: (...args: T) => void, args: T, delay: number): void;', 25 | }, 26 | { 27 | file: 'com/bitwig/extension/controller/api/NoteInput.d.ts', 28 | search: /table: object\[\]/gm, 29 | replace: 'table: number[]', 30 | }, 31 | { 32 | file: 'com/bitwig/extension/controller/api/Application.d.ts', 33 | search: /PANEL_LAYOUT_(.+): string;/gm, 34 | replace: "PANEL_$1_ARRANGE: '$1';", 35 | }, 36 | { 37 | file: 'com/bitwig/extension/controller/api/Application.d.ts', 38 | search: /static (\/\*\*)/gm, 39 | replace: '$1', 40 | }, 41 | ]; 42 | 43 | export const applyMods = () => { 44 | mods.forEach(({ file: relPath, search, replace }) => { 45 | const absPath = path.join(__dirname, '..', 'types', relPath); 46 | const file = fs.readFileSync(absPath, 'utf-8'); 47 | const result = file.replace(search, replace); 48 | fs.writeFileSync(absPath, result); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/renderers/util.ts: -------------------------------------------------------------------------------- 1 | import { AnyCstNode } from '../types'; 2 | import { visit } from '../visit'; 3 | 4 | export const typeOverrides: { [javaType: string]: string } = { 5 | String: 'string', 6 | Byte: 'number', 7 | Short: 'number', 8 | Integer: 'number', 9 | Long: 'number', 10 | Float: 'number', 11 | Double: 'number', 12 | Boolean: 'boolean', 13 | BigDecimal: 'number', 14 | BigInteger: 'number', 15 | Number: 'number', 16 | Void: 'void', 17 | Object: 'object', 18 | Runnable: '() => void', 19 | JSObject: '() => void', 20 | Future: 'unknown', 21 | StringBuilder: 'object', 22 | }; 23 | 24 | export const ignoredImports = [ 25 | 'java.util.Objects', 26 | 'java.util.Collections', 27 | 'java.util.ArrayList', 28 | 'java.util.concurrent.Callable', 29 | 'com.bitwig.extension.api.opensoundcontrol.OscNode', 30 | 'com.bitwig.extension.api.opensoundcontrol.OscMethod', 31 | 'java.util.concurrent.Future', 32 | 'java.io.IOException', 33 | 'java.lang.annotation.Retention', 34 | 'java.lang.annotation.RetentionPolicy', 35 | 'java.nio.charset.StandardCharsets', 36 | 'java.util.Arrays', 37 | 'jdk.nashorn.api.scripting.JSObject', 38 | ]; 39 | 40 | export const getTypeArgs = (node?: AnyCstNode): string => { 41 | if (!node) return ''; 42 | const typeArguments = visit(node, 'typeArgument', 'referenceType'); 43 | const args = typeArguments 44 | .map((ta) => { 45 | // @ts-ignore 46 | const isArrayOfType = ta.dims; 47 | const classType = ta.classOrInterfaceType?.[0].children.classType?.[0]; 48 | const name = classType?.children.Identifier?.[0].image!; 49 | 50 | if (typeOverrides[name]) 51 | return `${typeOverrides[name]}${isArrayOfType ? '[]' : ''}`; 52 | 53 | const args = classType?.children.typeArguments 54 | ? getTypeArgs(classType?.children.typeArguments?.[0]) 55 | : ''; 56 | 57 | return `${name}${args}${isArrayOfType ? '[]' : ''}`; 58 | }) 59 | .join(', '); 60 | 61 | return args ? `<${args}>` : ''; 62 | }; 63 | -------------------------------------------------------------------------------- /src/build.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | 4 | import tspkg from 'typescript/package.json'; 5 | import { parse } from 'java-parser'; 6 | import prettier from 'prettier'; 7 | 8 | import pkg from '../package.json'; 9 | import { visit } from './visit'; 10 | import { renderTypeDeclarations } from './renderers/renderTypeDeclarations'; 11 | import { renderImportDeclarations } from './renderers/renderImportDeclarations'; 12 | import { applyMods } from './mods'; 13 | import { renderEnumDeclarations } from './renderers/renderEnumDeclarations'; 14 | 15 | const STD_OUT = Symbol('STD_OUT'); 16 | 17 | function convert(src: string, dest?: typeof STD_OUT | string) { 18 | const sourceCode = fs.readFileSync(src).toString(); 19 | const rootNode = parse(sourceCode); 20 | const ordinaryCompilationUnits = visit(rootNode, 'ordinaryCompilationUnit'); 21 | const { packageDeclaration, importDeclaration, typeDeclaration } = 22 | ordinaryCompilationUnits?.[0] || {}; 23 | 24 | const namespace = packageDeclaration?.[0].children.Identifier?.map( 25 | (id) => id.image 26 | ).join('.'); 27 | 28 | const children = [ 29 | renderImportDeclarations(importDeclaration), 30 | renderEnumDeclarations(rootNode), 31 | renderTypeDeclarations(typeDeclaration), 32 | ] 33 | .filter((c) => c) 34 | .join('\n'); 35 | 36 | let result = namespace 37 | ? `declare namespace ${namespace} {\n${children}}\n` 38 | : children; 39 | 40 | result = prettier.format(result, { 41 | parser: 'typescript', 42 | singleQuote: true, 43 | }); 44 | 45 | if (dest) { 46 | if (dest === STD_OUT) { 47 | console.log(result); 48 | } else { 49 | fs.writeFileSync(dest, result); 50 | } 51 | } 52 | 53 | return result; 54 | } 55 | 56 | function convertAll(src: string, dest: string) { 57 | const files = fs.readdirSync(src); 58 | files.forEach(function (file) { 59 | const newSrc = path.join(src, file); 60 | const newDest = path.join(dest, file); 61 | if (fs.statSync(newSrc).isDirectory()) { 62 | convertAll(newSrc, newDest); 63 | } else { 64 | if (file.endsWith('.java')) { 65 | if (file.endsWith('Exception.java')) { 66 | return; 67 | } 68 | if (!fs.existsSync(dest)) { 69 | fs.mkdirSync(dest, { recursive: true }); 70 | } 71 | const from = newSrc; 72 | const to = newDest.replace('.java', '.d.ts'); 73 | console.log( 74 | '◍ ./' + 75 | path.relative(path.join(__dirname, '..'), from) + 76 | '\n' + 77 | '┗━━━━► ./' + 78 | path.relative(path.join(__dirname, '..'), to) 79 | ); 80 | convert(newSrc, newDest.replace('.java', '.d.ts')); 81 | console.log(''); 82 | } 83 | } 84 | }); 85 | } 86 | 87 | type Declaration = { 88 | filename: string; 89 | filePath: string; 90 | namespace: string; 91 | declaration: string; 92 | }; 93 | 94 | function getDeclarations(src: string): Declaration[] { 95 | const declarations: Declaration[] = []; 96 | 97 | for (const nodeName of fs.readdirSync(src)) { 98 | const nodePath = path.join(src, nodeName); 99 | if (fs.statSync(nodePath).isDirectory()) { 100 | declarations.push(...getDeclarations(nodePath)); 101 | } else if (nodeName.endsWith('.d.ts')) { 102 | const fullDeclaration = fs.readFileSync(nodePath, 'utf-8'); 103 | const namespace = fullDeclaration.match(/namespace (.+) {/m)?.[1]; 104 | const declarationSansNamespace = fullDeclaration 105 | .trim() 106 | .split('\n') 107 | .slice(1, -1) 108 | .join('\n'); 109 | if (!namespace) throw new Error('Unable to find namespace'); 110 | declarations.push({ 111 | filename: nodeName, 112 | filePath: nodePath.split(path.join(__dirname, '..', 'types') + '/')[1], 113 | namespace, 114 | declaration: declarationSansNamespace, 115 | }); 116 | } 117 | } 118 | 119 | return declarations; 120 | } 121 | 122 | function bundleDeclarations(srcDir: string, outFile: string) { 123 | const declarations = getDeclarations(srcDir).reduce<{ 124 | [namespace: string]: Declaration[]; 125 | }>((acc, dec) => { 126 | if (!acc[dec.namespace]) acc[dec.namespace] = []; 127 | acc[dec.namespace].push(dec); 128 | return acc; 129 | }, {}); 130 | 131 | const namespaces = Object.keys(declarations).sort(); 132 | let result = `\ 133 | // Type definitions for Bitwig Studio Control Surface Scripting API v${ 134 | pkg.version.split('.')[0] 135 | } 136 | // Project: https://bitwig.com 137 | // Definitions by: Joseph Larson 138 | // TypeScript Version: ${tspkg.version}\n\n`; 139 | 140 | result += namespaces 141 | .map((ns) => { 142 | const nsDeclarations = declarations[ns] 143 | .sort((a, b) => a.filename.localeCompare(b.filename)) 144 | .map((dec) => { 145 | const comment = `// source: ${dec.filePath.replace( 146 | '.d.ts', 147 | '.java' 148 | )}`; 149 | return `${comment}\n\n${dec.declaration}\n`; 150 | }) 151 | .join('\n\n'); 152 | 153 | // remove duplicate imports 154 | const imports = new Set(); 155 | const dedupedImports = nsDeclarations 156 | .split('\n') 157 | .filter((l) => { 158 | const trimmedL = l.trim(); 159 | const importMatches = /^import \S+ = (\S+);/.exec(trimmedL); 160 | 161 | // skip non import lines 162 | if (!importMatches) return true; 163 | 164 | // Remove imports from the current namespace as they are not needed and conflict with the definition when bundling 165 | // Example: pulls 'com.bitwig.extension.api.graphics' out of 166 | // 'import GraphicsOutput = com.bitwig.extension.api.graphics.GraphicsOutput' 167 | const importNs = importMatches[1].split('.').slice(0, -1).join('.'); 168 | if (importNs === ns) return false; 169 | 170 | // Remove duplicate imports within a namespace 171 | if (imports.has(trimmedL)) { 172 | return false; 173 | } else { 174 | imports.add(trimmedL); 175 | return true; 176 | } 177 | }) 178 | .join('\n'); 179 | 180 | return `declare namespace ${ns} {\n${dedupedImports}\n}\n`; 181 | }) 182 | .join('\n'); 183 | result = 184 | result + 185 | '\n' + 186 | fs.readFileSync( 187 | path.join(__dirname, '..', 'src', 'additional-types.d.ts.template'), 188 | 'utf-8' 189 | ); 190 | try { 191 | result = prettier.format(result, { 192 | parser: 'typescript', 193 | singleQuote: true, 194 | }); 195 | } catch (e) { 196 | console.error('Failed to prettify:', e); 197 | } 198 | fs.writeFileSync(outFile, result); 199 | } 200 | 201 | const SRC_DIR = path.join(__dirname, '..', 'java_source'); 202 | const DEST_DIR = path.join(__dirname, '..', 'types'); 203 | 204 | export async function build() { 205 | convertAll(SRC_DIR, DEST_DIR); 206 | applyMods(); 207 | bundleDeclarations(DEST_DIR, path.join(__dirname, '..', 'bitwig-api.d.ts')); 208 | fs.rmSync(DEST_DIR, { recursive: true }); 209 | 210 | console.log('Bundled into bitwig-api.d.ts'); 211 | } 212 | 213 | if (require.main === module) build(); 214 | -------------------------------------------------------------------------------- /src/renderers/renderTypeDeclarations/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ConstructorDeclarationCstNode, 3 | InterfaceMethodDeclarationCstNode, 4 | MethodDeclarationCstNode, 5 | ResultCtx, 6 | TypeDeclarationCstNode, 7 | } from 'java-parser'; 8 | import { AnyCstNode } from '../../types'; 9 | import { visit } from '../../visit'; 10 | import { getTypeArgs, typeOverrides } from '../util'; 11 | import { renderUnannType } from './renderUnannType'; 12 | 13 | const getTypeKind = ( 14 | node: TypeDeclarationCstNode 15 | ): 'interface' | 'class' | 'enum' => { 16 | if (node.children.interfaceDeclaration) { 17 | return 'interface'; 18 | } else if ( 19 | node.children.classDeclaration?.[0].children.normalClassDeclaration 20 | ) { 21 | return 'class'; 22 | } else if (node.children.classDeclaration?.[0].children.enumDeclaration) { 23 | return 'enum'; 24 | } else { 25 | throw new Error('Unhandled type kind'); 26 | } 27 | }; 28 | 29 | const getTypeDocstring = (node: TypeDeclarationCstNode) => { 30 | return node.leadingComments?.[0]?.image as string | undefined; 31 | }; 32 | 33 | const getTypeParams = (node: AnyCstNode): string => { 34 | const typeParams = visit(node, 'typeParameter'); 35 | const params = typeParams 36 | .map((tp) => { 37 | const paramName = tp.typeIdentifier?.[0].children.Identifier?.[0].image; 38 | const extendsName = 39 | tp.typeBound?.[0].children.classOrInterfaceType?.[0].children 40 | .classType?.[0].children.Identifier?.[0].image; 41 | const extendsArgs = extendsName 42 | ? getTypeArgs( 43 | tp.typeBound?.[0].children.classOrInterfaceType?.[0].children 44 | .classType?.[0]! 45 | ) 46 | : ''; 47 | 48 | return `${paramName}${ 49 | extendsName 50 | ? ` extends ${extendsName}${extendsArgs} = ${extendsName}${extendsArgs}` 51 | : '' 52 | }`; 53 | }) 54 | .join(', '); 55 | 56 | return params ? `<${params}>` : ''; 57 | }; 58 | 59 | const getTypeName = (node: TypeDeclarationCstNode) => { 60 | const typeName = ( 61 | visit(node, 'normalInterfaceDeclaration')?.[0] || 62 | visit(node, 'normalClassDeclaration')?.[0] || 63 | visit(node, 'enumDeclaration')?.[0] 64 | )?.typeIdentifier?.[0].children.Identifier?.[0].image; 65 | 66 | const typeParams = getTypeParams(node); 67 | 68 | if (!typeName) { 69 | throw new Error('Failed to get type name'); 70 | } 71 | 72 | return `${typeName}${typeParams}`; 73 | }; 74 | 75 | const getTypeParents = (node: TypeDeclarationCstNode): string[] => { 76 | const interfaceParents = node.children.interfaceDeclaration?.[0].children?.normalInterfaceDeclaration?.[0].children.extendsInterfaces?.[0].children.interfaceTypeList?.[0].children.interfaceType?.map( 77 | (extInt) => { 78 | const name = extInt.children.classType[0].children.Identifier[0].image; 79 | const typeArgs = getTypeArgs(extInt); 80 | 81 | return `${name}${typeArgs}`; 82 | } 83 | ); 84 | const classParent = 85 | node.children?.classDeclaration?.[0]?.children.normalClassDeclaration?.[0] 86 | .children?.superclass?.[0].children.classType?.[0].children 87 | .Identifier?.[0].image; 88 | const classParentArgs = getTypeArgs( 89 | node.children?.classDeclaration?.[0]?.children.normalClassDeclaration?.[0] 90 | .children?.superclass?.[0].children.classType?.[0] 91 | ); 92 | if (interfaceParents?.[0] === 'ObjectProxy') { 93 | console.log('test'); 94 | } 95 | return interfaceParents 96 | ? interfaceParents 97 | : classParent 98 | ? [`${classParent}${classParentArgs}`] 99 | : []; 100 | }; 101 | 102 | const getResultType = (node: ResultCtx) => { 103 | if (node.Void) { 104 | return 'void'; 105 | } else if (node.unannType?.[0]) { 106 | return renderUnannType(node.unannType?.[0]); 107 | } else { 108 | throw new Error('Unhandled result type'); 109 | } 110 | }; 111 | 112 | const renderMethodDeclaration = ( 113 | node?: InterfaceMethodDeclarationCstNode | MethodDeclarationCstNode, 114 | isCallback?: boolean 115 | ) => { 116 | if (!node) return ''; 117 | const isStatic = 118 | 'methodModifier' in node.children && 119 | !!node.children.methodModifier?.find((n) => n.children.Static); 120 | const name = 121 | node.children.methodHeader?.[0].children.methodDeclarator?.[0].children 122 | .Identifier?.[0].image; 123 | const result = getResultType( 124 | node.children.methodHeader?.[0].children.result?.[0].children 125 | ); 126 | const args = visit(node, 'formalParameter') 127 | .map((ctx) => { 128 | const isArityType = !!ctx.variableArityParameter; 129 | let name = 130 | ctx.variableParaRegularParameter?.[0].children.variableDeclaratorId?.[0] 131 | .children.Identifier?.[0].image || 132 | ctx.variableArityParameter?.[0].children.Identifier?.[0].image; 133 | if (name === 'function') name = 'func'; 134 | const type = renderUnannType( 135 | ctx.variableParaRegularParameter?.[0].children.unannType?.[0] || 136 | ctx.variableArityParameter?.[0]?.children?.unannType?.[0]! 137 | ); 138 | return `${isArityType ? '...' : ''}${name}: ${type}${ 139 | isArityType ? '[]' : '' 140 | }`; 141 | }) 142 | .join(', '); 143 | 144 | return `${isStatic ? 'static ' : ''}${ 145 | isCallback ? '' : name 146 | }(${args}): ${result}\n`; 147 | }; 148 | 149 | const renderConstructorDeclaration = (node?: ConstructorDeclarationCstNode) => { 150 | if (!node) return ''; 151 | const args = visit(node, 'variableParaRegularParameter') 152 | .map((ctx) => { 153 | const name = ctx.variableDeclaratorId?.[0].children.Identifier?.[0].image; 154 | const type = renderUnannType(ctx.unannType?.[0]); 155 | return `${name}: ${type}`; 156 | }) 157 | .join(', '); 158 | 159 | return `constructor(${args})\n`; 160 | }; 161 | 162 | const renderTypeBody = (node: TypeDeclarationCstNode, isCallback?: boolean) => { 163 | const interfaceBody = visit(node, 'interfaceBody')?.[0] 164 | ?.interfaceMemberDeclaration; 165 | const classBody = visit(node, 'classBody')?.[0]?.classBodyDeclaration; 166 | 167 | const members: string[] = []; 168 | if (interfaceBody) { 169 | interfaceBody.forEach((member) => { 170 | if (member.children.interfaceMethodDeclaration?.[0]) { 171 | // handle methods 172 | // @ts-ignore 173 | const docstring = member?.leadingComments?.[0].image; 174 | const method = renderMethodDeclaration( 175 | member.children.interfaceMethodDeclaration[0], 176 | isCallback 177 | ); 178 | 179 | members.push([docstring, method].filter((m) => m).join('\n')); 180 | } else if (member.children.constantDeclaration?.[0]) { 181 | const docstring = member?.leadingComments?.[0].image; 182 | const name = visit(member, 'variableDeclaratorId')?.[0].Identifier?.[0] 183 | .image; 184 | const type = renderUnannType( 185 | visit(member, 'constantDeclaration')?.[0].unannType?.[0] 186 | ); 187 | const isStatic = !!visit( 188 | member, 189 | 'constantDeclaration' 190 | )?.[0].constantModifier?.find((n) => n.children.Static); 191 | members.push( 192 | `${isStatic ? 'static ' : ''}${ 193 | docstring ? `${docstring}\n` : '' 194 | }${name}: ${type};\n` 195 | ); 196 | } else if (member.children.classDeclaration) { 197 | if ( 198 | member.children.classDeclaration?.[0].children.normalClassDeclaration 199 | ) { 200 | console.warn('Warning: Skipped inner class declaration.'); 201 | } 202 | } else if (member.children.interfaceDeclaration) { 203 | console.warn('Warning: Skipped inner interface declaration.'); 204 | } else if ( 205 | Object.keys(member.children).length === 1 && 206 | member.children.Semicolon 207 | ) { 208 | return ''; 209 | } else { 210 | console.log(node); 211 | throw new Error('Unhandled interface member type'); 212 | } 213 | }); 214 | } else if (classBody) { 215 | classBody.map((member) => { 216 | if ( 217 | member.children.classMemberDeclaration?.[0].children 218 | .methodDeclaration?.[0] 219 | ) { 220 | // handle methods 221 | // @ts-ignore 222 | const docstring = member?.leadingComments?.[0].image; 223 | const method = renderMethodDeclaration( 224 | member.children.classMemberDeclaration[0].children 225 | .methodDeclaration[0], 226 | isCallback 227 | ); 228 | 229 | members.push([docstring, method].filter((m) => m).join('\n')); 230 | } else if (member.children.constructorDeclaration?.[0]) { 231 | const docstring = member?.leadingComments?.[0].image; 232 | const constructor = renderConstructorDeclaration( 233 | member.children.constructorDeclaration?.[0] 234 | ); 235 | members.push([docstring, constructor].filter((m) => m).join('\n')); 236 | } else if ( 237 | member.children.classMemberDeclaration?.[0].children.fieldDeclaration 238 | ) { 239 | const docstring = member?.leadingComments?.[0].image; 240 | const name = visit(member, 'variableDeclaratorId')?.[0].Identifier?.[0] 241 | .image; 242 | const isStatic = !!visit( 243 | member, 244 | 'fieldDeclaration' 245 | )?.[0].fieldModifier?.find((n) => n.children.Static); 246 | 247 | const type = renderUnannType( 248 | visit(member, 'fieldDeclaration')?.[0].unannType?.[0] 249 | ); 250 | members.push( 251 | `${isStatic ? 'static ' : ''}${ 252 | docstring ? `${docstring}\n` : '' 253 | }${name}: ${type};\n` 254 | ); 255 | } else { 256 | throw new Error('Unhandled class member type'); 257 | } 258 | }); 259 | } 260 | 261 | return members.join('\n'); 262 | }; 263 | 264 | const renderTypeDeclaration = (node: TypeDeclarationCstNode) => { 265 | if ( 266 | node.children.interfaceDeclaration?.[0].children.annotationTypeDeclaration 267 | ) { 268 | console.warn('Warning: Skipping annotation type.'); 269 | return ''; 270 | } 271 | const type = { 272 | kind: getTypeKind(node), 273 | docstring: getTypeDocstring(node), 274 | name: getTypeName(node), 275 | parents: getTypeParents(node), 276 | }; 277 | 278 | // handle enums separately 279 | if (type.kind === 'enum') return ''; 280 | 281 | const isCallback = 282 | type.name.endsWith('Callback') || type.name.includes('Callback<'); 283 | 284 | return `${type.docstring ? `${type.docstring}\n` : ''}${type.kind} ${ 285 | type.name 286 | }${ 287 | type.parents.length ? ` extends ${type.parents.join(',')}` : '' 288 | } {\n${renderTypeBody(node, isCallback)}}\n`; 289 | }; 290 | 291 | export function renderTypeDeclarations(nodes?: TypeDeclarationCstNode[]) { 292 | if (!nodes) return ''; 293 | 294 | const types = nodes.map(renderTypeDeclaration); 295 | 296 | return types.join('\n'); 297 | } 298 | --------------------------------------------------------------------------------