Hone logo
Hone
Problems

TypeScript Subpath Exports Implementation

You're tasked with enhancing a TypeScript library to support subpath exports. This feature allows consumers of your library to import specific modules or parts of your library using distinct import paths, rather than just importing the entire library. This improves module granularity, tree-shaking, and developer experience.

Problem Description

Your goal is to implement subpath exports for a given TypeScript package. This involves configuring your package.json to define these export mappings and ensuring that your TypeScript compilation and packaging process correctly generates the necessary output for these subpaths.

Key Requirements:

  1. Define Subpath Exports: Configure package.json to expose specific internal modules or groups of modules under distinct export paths. For instance, you might want to export a utils module under the path your-package/utils.
  2. TypeScript Compilation: Ensure that TypeScript is configured to understand and process these subpath exports, especially for internal references within the package.
  3. Output Structure: The compiled output (e.g., JavaScript files) should be structured in a way that supports these subpath exports when the package is published. This typically means having corresponding directories or files for each exported subpath.
  4. Expose Internal Modules: You should be able to export specific internal modules or directories as distinct subpaths.

Expected Behavior:

When a user imports from your package, they should be able to:

  • Import the main entry point (e.g., import * as main from 'your-package';)
  • Import from a subpath (e.g., import { someUtil } from 'your-package/utils';)

Edge Cases to Consider:

  • Internal References: How do internal modules within your package reference each other, especially when subpath exports are involved?
  • Conditional Exports: While not strictly required for this challenge, consider how you might (or if you should) handle different environments (e.g., Node.js vs. browser) with subpath exports (though focus on the basic implementation for now).
  • Type Definitions: Ensure that TypeScript type definitions (.d.ts files) are correctly generated and accessible for subpath exports.

Examples

Let's assume a package named my-awesome-lib.

Example 1: Basic Subpath Export

Project Structure:

my-awesome-lib/
├── src/
│   ├── index.ts          // Main entry point
│   └── utils/
│       ├── index.ts      // Exports for 'my-awesome-lib/utils'
│       └── string.ts     // A utility function
├── package.json
└── tsconfig.json

src/utils/string.ts:

export function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1);
}

src/utils/index.ts:

export * from './string';

src/index.ts:

export const greeting = "Hello from my-awesome-lib!";

package.json (with exports):

{
  "name": "my-awesome-lib",
  "version": "1.0.0",
  "main": "dist/index.js", // For older module systems
  "types": "dist/index.d.ts", // Main types
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./utils": {
      "import": "./dist/utils/index.mjs",
      "require": "./dist/utils/index.js",
      "types": "./dist/utils/index.d.ts"
    }
  },
  // ... other fields
}

Usage in another project:

// Import from main entry
import { greeting } from 'my-awesome-lib';
console.log(greeting); // Output: Hello from my-awesome-lib!

// Import from subpath
import { capitalize } from 'my-awesome-lib/utils';
console.log(capitalize('world')); // Output: World

Explanation: The package.json exports field is configured to map the root of the package (.) to dist/index.js and dist/index.mjs, and the utils subpath (./utils) to dist/utils/index.js and dist/utils/index.mjs. This allows consumers to import directly from my-awesome-lib/utils.

Example 2: Subpath Export with Nested Modules

Project Structure:

my-awesome-lib/
├── src/
│   ├── index.ts
│   └── modules/
│       ├── math/
│       │   ├── index.ts
│       │   └── add.ts
│       └── text/
│           ├── index.ts
│           └── reverse.ts
├── package.json
└── tsconfig.json

src/modules/math/add.ts:

export function add(a: number, b: number): number {
  return a + b;
}

src/modules/math/index.ts:

export * from './add';

src/modules/text/reverse.ts:

export function reverseString(str: string): string {
  return str.split('').reverse().join('');
}

src/modules/text/index.ts:

export * from './reverse';

src/index.ts:

export const libName = "My Awesome Lib";

package.json (with exports):

{
  "name": "my-awesome-lib",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./modules/math": {
      "import": "./dist/modules/math/index.mjs",
      "require": "./dist/modules/math/index.js",
      "types": "./dist/modules/math/index.d.ts"
    },
    "./modules/text": {
      "import": "./dist/modules/text/index.mjs",
      "require": "./dist/modules/text/index.js",
      "types": "./dist/modules/text/index.d.ts"
    }
  },
  // ... other fields
}

Usage in another project:

import { libName } from 'my-awesome-lib';
console.log(libName); // Output: My Awesome Lib

import { add } from 'my-awesome-lib/modules/math';
console.log(add(5, 3)); // Output: 8

import { reverseString } from 'my-awesome-lib/modules/text';
console.log(reverseString('typescript')); // Output: tpircsepyt

Explanation: The exports field now defines mappings for both ./modules/math and ./modules/text to their respective compiled entry points within the dist directory. This allows consumers to import functionalities grouped under these distinct subpaths.

Constraints

  • The primary focus is on Node.js environments and modern module bundlers that support the exports field.
  • You must use package.json's exports field to define the subpath mappings.
  • The output should be compatible with CommonJS (.js) and ES Modules (.mjs).
  • TypeScript compilation should be configured to generate both JavaScript and declaration files (.d.ts).
  • Your solution should demonstrate the export of at least two distinct subpaths.

Notes

  • Consider using a build tool like npm script, esbuild, rollup, or webpack to compile your TypeScript code and bundle it appropriately for the specified exports targets.
  • Pay close attention to the tsconfig.json configuration, especially module, outDir, and declaration.
  • The main and types fields in package.json should still point to the main entry point for backward compatibility with older Node.js versions and tooling.
  • When defining exports, you can specify different targets for require (CommonJS) and import (ES Modules).
  • Ensure that internal references within your library correctly resolve to the compiled output, not directly to the src files. You might need to adjust tsconfig.json's baseUrl or path mappings if you have complex internal imports.
Loading editor...
typescript