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:
- Define Subpath Exports: Configure
package.jsonto expose specific internal modules or groups of modules under distinct export paths. For instance, you might want to export autilsmodule under the pathyour-package/utils. - TypeScript Compilation: Ensure that TypeScript is configured to understand and process these subpath exports, especially for internal references within the package.
- 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.
- 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.tsfiles) 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
exportsfield. - You must use
package.json'sexportsfield 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, orwebpackto compile your TypeScript code and bundle it appropriately for the specifiedexportstargets. - Pay close attention to the
tsconfig.jsonconfiguration, especiallymodule,outDir, anddeclaration. - The
mainandtypesfields inpackage.jsonshould 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 forrequire(CommonJS) andimport(ES Modules). - Ensure that internal references within your library correctly resolve to the compiled output, not directly to the
srcfiles. You might need to adjusttsconfig.json'sbaseUrlor path mappings if you have complex internal imports.