Search code examples
typescriptdeclarationdefinitelytyped

How to contribute multiple declaration files and folders to DefinitelyTyped


I have written TypeScript declaration files for the PlayCanvas game engine. The source code has a global structure under the pc namespace. I have copied the source code's file and folder structure when writing the declaration files. I have over 50 separate d.ts files and they all use declare namespace pc {}. I would now like to contribute these declaration files to DefinitelyTyped but looking at the docs it seems like they might want everything in just one big file called index.d.ts file. How can I convert all my files and folders so that the whole thing is compatible with DefinitelyTyped? Do I really have to cram everything into one file? Can't I just keep my nice file and folder structure that matches the engine's source code?


Solution

  • This is a common pain point with creating type definitions for large libraries. The best solution at this point is to use a utility, like dts-generator, to generate an index.d.ts for distribution/consumption (and keep your individual files/folders as a source).

    dts-generator semi-blindly dumps everything into one file. In my case (which also happens to be a game engine, you can see here), the file needed some post-processing to be compatible with my index.ts file that re-exported different parts of the library. This is a command line utility that can be run with ts-node (npm i ts-node -g).

    import * as fs from 'fs';
    import * as readline from 'readline';
    
    const moduleDeclaration = 'module-typings.d.ts'; // file created by dts-generator
    const indexDeclartion = 'index.d.ts';
    const distPath = './dist/';
    
    const indexOut: string[] = [];
    
    const reader = readline.createInterface({
        input: fs.createReadStream(`${distPath}${moduleDeclaration}`),
    });
    
    const moduleDeclaration = /^declare module/;
    const malletImport = /(import|export).+from 'mallet/;
    
    function isExcluded(line: string) {
        return moduleDeclaration.exec(line) || malletImport.exec(line) || line === '}';
    }
    
    const identifiers = [];
    const importLine = /import {(.*)} (.*)/;
    const importRequire = /import (.*) = (.*)/;
    function stripDuplicateDeclarations(line) {
        let importResult;
        if ((importResult = importLine.exec(line))) { // tslint:disable-line:no-conditional-assignment
            const imports = importResult[1].replace(/\s/g, '').split(',');
            const newImports = imports.filter((variable) => identifiers.indexOf(variable) === -1);
            Array.prototype.push.apply(identifiers, newImports);
            const importString = newImports.join(', ');
            return line.replace(importLine, `import {${importString}} $2`);
        } else if ((importResult = importRequire.exec(line))) { // tslint:disable-line:no-conditional-assignment
            const importName = importResult[1];
            if (identifiers.indexOf(importName) === -1) {
                identifiers.push(importName);
                return line;
            }
    
            return ''; // return an empty line if the import exists
        }
    
        return line;
    }
    
    const constLine = /^\s{2}const .+:/;
    function declareConst(line) {
        if (constLine.exec(line) !== null) {
            return (`declare${line}`).replace(/\s+/, ' ');
        }
    
        return line;
    }
    
    reader.on('line', (line) => {
        if (!isExcluded(line)) {
            const finalLine = [stripDuplicateDeclarations, declareConst]
                .reduce((processedLine, processor) => processor(processedLine), line);
            indexOut.push(finalLine);
        }
    });
    
    reader.on('close', () => {
        const indexContext = indexOut.join('\n');
        fs.writeFileSync(`${distPath}${indexDeclartion}`, indexContext, {encoding: 'utf-8'});
    });
    

    This script strips out duplicate declarations and nested namespaces so consumers can easily import library components. As a disclaimer, this script is only an example that worked for my library, and isn't intended handle all cases. There is probably a more correct (and complex) way to do this kind of processing with the TypeScript API.