How do I generate correct types for my package?

494 Views Asked by At

I am trying to generate an npm package, so that other devs in my company can use it. I've almost got everything working, but I can't seem to get the declarations right.

I've tried a number of approaches to generating the file, but in a nutshell when I generate declaration files with tsc, I have two options:

  • generate one file as the output, or
  • many, which would have the same folder structure as my source code.

Both of these formats have not been useful to my client application. When I generate the single file, my client app complains that the file "is not a module". When I generate multiple files, there's no "index.d.ts" top-level file, so my client app complains. How should I resolve this?

My npm commands:

"prebuild": "rm -rf ./dist",
"build": "rollup -c",
"postbuild": "tsc --project tsconfig.dts.json"

My rollup config:

import alias from '@rollup/plugin-alias';
import autoprefixer from 'autoprefixer';
import babel from '@rollup/plugin-babel';
import copy from 'rollup-plugin-copy';
import commonjs from '@rollup/plugin-commonjs';
import esbuild from 'rollup-plugin-esbuild';
import external from 'rollup-plugin-peer-deps-external';
import filesize from 'rollup-plugin-filesize';
import postcss from 'rollup-plugin-postcss';
import resolve from '@rollup/plugin-node-resolve';
import svgr from '@svgr/rollup';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

const extensions = ['.js', '.jsx', '.ts', '.tsx'];

const rollup = [
    {
        input: './src/components/index.ts',
        output: [
            {
                file: 'dist/index.js',
                format: 'cjs',
                sourcemap: true,
                exports: 'named',
            },
            {
                file: 'dist/index.es.js',
                format: 'es',
                sourcemap: true,
                exports: 'named',
            },
        ],
        external: ['react', 'react-dom'],
        plugins: [
            external(),
            alias({
                entries: [
                    {
                        // Replace @/ with src/
                        find: /^@\/(.*)$/,
                        replacement:
                            dirname(fileURLToPath(import.meta.url)) + '/src/$1',
                    },
                ],
            }),
            resolve({
                extensions,
            }),
            commonjs(),
            svgr({
                exportType: 'named',
                namedExport: 'ReactComponent',
                exportAll: true,
                babel: true,
            }),
            babel({
                extensions,
                include: ['src/**/*'],
                babelHelpers: 'runtime',
                presets: [
                    '@babel/preset-env',
                    '@babel/preset-react',
                    '@babel/preset-typescript',
                ],
            }),
            copy({
                targets: [{ src: 'src/assets', dest: 'dist', flatten: false }],
            }),
            esbuild({
                include: extensions.map((ext) => `src/**/*${ext}`),
                minify: process.env.NODE_ENV === 'production',
                optimizeDeps: {
                    include: [
                        '@mui/base',
                        '@mui/icons-material',
                        '@mui/material',
                        'uuid',
                    ],
                },
            }),
            postcss({
                extensions: ['.css'],
                extract: 'styles.css',
                plugins: [autoprefixer()],
            }),
            filesize(),
        ],
    },
];

export default rollup;

My tsconfig.json:

{
    "compilerOptions": {
        "jsx": "react-jsx",
        "allowJs": false,
        "target": "esnext",
        "module": "esnext",
        "lib": ["dom", "dom.iterable", "esnext"],
        // * output .js.map sourcemap files for consumers
        "sourceMap": true,
        "rootDir": "./",
        "skipLibCheck": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "esModuleInterop": true,
        "moduleResolution": "node",
        "baseUrl": ".",
        "paths": {
            "@/*": ["src/*"]
        }
    },
    "include": [
        "src/**/*",
        "globals.d.ts"
    ],
    "exclude": [
        "dist",
        "node_modules",
        "build",
        "src/**/*.stories.ts?(x)",
        "src/**/*.mock.ts?(x)",
        "src/**/*.test.ts?(x)",
        "src/app/**"
    ]
}

My tsconfig.dts.json:

{
    "compilerOptions": {
        "outFile": "./dist/types/index.d.ts",
        // outDir: "./dist/types",
        "jsx": "react-jsx",
        "lib": ["dom", "dom.iterable", "es6"],
        "moduleResolution": "Node",
        "esModuleInterop": true,
        "baseUrl": "./",
        "paths": {
            "@/*": ["src/*"],
        },
        "allowJs": true,
        // Generate d.ts files
        "declaration": true,
        // This compiler run should
        // only output d.ts files
        "emitDeclarationOnly": true,
        // go to js file when using IDE functions like
        // "Go to Definition" in VSCode
        "declarationMap": true
    },
    "include": [
        "src/**/*",
        "globals.d.ts"
    ],
    "exclude": [
        "src/**/*.stories.ts?(x)",
        "src/**/*.mock.ts?(x)",
        "src/**/*.test.ts?(x)",
        "src/app/**"
    ]
}

And here's a sample of what tsc generates as output (for the single index.d.ts case):

/// <reference types="react" />
declare module "assets/empty/index" {
    export { ReactComponent as IllyEmptyDesert } from './illy_empty_desert.svg';
}
declare module "assets/flags/index" {
    import { ReactComponent as Canada } from './Canada.svg';
    import { ReactComponent as United_States_of_America } from './United_States_of_America.svg';
    export { Canada, United_States_of_America, };
}
declare module "components/AdditionalResults/AdditionalResults.definition" {
    import { StoryObj } from '@storybook/react';
    import { AdditionalResults } from "components/AdditionalResults/AdditionalResults";
    export type AdditionalResultsStory = StoryObj<typeof AdditionalResults>;
    export interface Result {
        id: number;
        highlighted?: boolean;
        value?: string;
    }
    export interface AdditionalResultsProps {
        mode?: string;
        disabled?: boolean;
        additionalResults?: Result[];
    }
}

Any help/advice would be a life saver!

1

There are 1 best solutions below

0
BenZ On

So, I think I've gotten it resolved, and hopefully this helps someone out there someday.

In a nutshell, I posted this question to the Typescript Discord server, and got an answer that lead me to the solution(s):

When I generate multiple files, there's no "index.d.ts" top-level file, so my client app complains. How should I resolve this?

your library should still have an entry point exporting your components and that is this file's typing that should be the entry point for your typings and that you should reference in your package.json

With that, I modified 2 things:

  • instead of trying to emit 1 file, I emitted one per component, by setting the tsconfig.dts.json from:
    "compilerOptions": {
        "outFile": "./dist/types/index.d.ts",

to

    "compilerOptions": {
        "outDir": "./dist/types",
  • and then changing the "types" in the package.json:
"types": "dist/types/components/index.d.ts",
    "exports": {
        ".": {
            "types": "./dist/types/components/index.d.ts",