From 61fc06c6130391847a370a3efc1c6e7677ba6596 Mon Sep 17 00:00:00 2001 From: ocavue Date: Fri, 17 Nov 2023 20:29:07 +0800 Subject: [PATCH] feat: add experimental dts rollup using @microsoft/api-extractor (#983) --- docs/README.md | 12 ++ package.json | 5 + pnpm-lock.yaml | 260 ++++++++++++++++++++++- src/api-extractor.ts | 155 ++++++++++++++ src/cli-main.ts | 1 + src/esbuild/postcss.ts | 7 +- src/exports.ts | 139 +++++++++++++ src/index.ts | 47 ++++- src/options.ts | 22 +- src/rollup.ts | 44 +--- src/tsc.ts | 218 +++++++++++++++++++ src/utils.ts | 97 ++++++++- test/__snapshots__/index.test.ts.snap | 289 ++++++++++++++++++++++++++ test/index.test.ts | 173 ++++++++++++++- 14 files changed, 1395 insertions(+), 74 deletions(-) create mode 100644 src/api-extractor.ts create mode 100644 src/exports.ts create mode 100644 src/tsc.ts diff --git a/docs/README.md b/docs/README.md index 9641ef0c3..69324005d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -193,6 +193,16 @@ If you have multiple entry files, each entry will get a corresponding `.d.ts` fi Note that `--dts` does not resolve external (aka in `node_modules`) types used in the `.d.ts` file, if that's somehow a requirement, try the experimental `--dts-resolve` flag instead. +Since tsup version 7.4.0, you can also use `--experimental-dts` flag to generate declaration files. This flag use [@microsoft/api-extractor](https://www.npmjs.com/package/@microsoft/api-extractor) to generate declaration files, which is more reliable than the previous `--dts` flag. It's still experimental and we are looking for feedbacks. + +To use `--experimental-dts`, you would need to install `@microsoft/api-extractor`, as it's a peer dependency of tsup: + +```bash +npm i @microsoft/api-extractor -D +# Or Yarn +yarn add @microsoft/api-extractor --dev +``` + #### Emit declaration file only The `--dts-only` flag is the equivalent of the `emitDeclarationOnly` option in `tsc`. Using this flag will only emit the declaration file, without the JavaScript files. @@ -500,6 +510,8 @@ esbuild has [experimental CSS support](https://esbuild.github.io/content-types/# To use PostCSS, you need to install PostCSS: ```bash +npm i postcss -D +# Or Yarn yarn add postcss --dev ``` diff --git a/package.json b/package.json index e6d0d279a..74a4b347a 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "tree-kill": "^1.2.2" }, "devDependencies": { + "@microsoft/api-extractor": "^7.38.3", "@rollup/plugin-json": "6.0.1", "@swc/core": "1.2.218", "@types/debug": "4.1.7", @@ -74,6 +75,7 @@ }, "peerDependencies": { "@swc/core": "^1", + "@microsoft/api-extractor": "^7.36.0", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, @@ -86,6 +88,9 @@ }, "@swc/core": { "optional": true + }, + "@microsoft/api-extractor": { + "optional": true } }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd222eb67..8748bc968 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,9 @@ dependencies: version: 1.2.2 devDependencies: + '@microsoft/api-extractor': + specifier: ^7.38.3 + version: 7.38.3(@types/node@14.18.12) '@rollup/plugin-json': specifier: 6.0.1 version: 6.0.1(rollup@4.0.2) @@ -560,7 +563,7 @@ packages: engines: {node: '>=6.0.0'} dependencies: '@jridgewell/set-array': 1.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.4.14 '@jridgewell/trace-mapping': 0.3.17 dev: true @@ -596,6 +599,49 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /@microsoft/api-extractor-model@7.28.2(@types/node@14.18.12): + resolution: {integrity: sha512-vkojrM2fo3q4n4oPh4uUZdjJ2DxQ2+RnDQL/xhTWSRUNPF6P4QyrvY357HBxbnltKcYu+nNNolVqc6TIGQ73Ig==} + dependencies: + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 3.61.0(@types/node@14.18.12) + transitivePeerDependencies: + - '@types/node' + dev: true + + /@microsoft/api-extractor@7.38.3(@types/node@14.18.12): + resolution: {integrity: sha512-xt9iYyC5f39281j77JTA9C3ISJpW1XWkCcnw+2vM78CPnro6KhPfwQdPDfwS5JCPNuq0grm8cMdPUOPvrchDWw==} + hasBin: true + dependencies: + '@microsoft/api-extractor-model': 7.28.2(@types/node@14.18.12) + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 3.61.0(@types/node@14.18.12) + '@rushstack/rig-package': 0.5.1 + '@rushstack/ts-command-line': 4.17.1 + colors: 1.2.5 + lodash: 4.17.21 + resolve: 1.22.1 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.0.4 + transitivePeerDependencies: + - '@types/node' + dev: true + + /@microsoft/tsdoc-config@0.16.2: + resolution: {integrity: sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw==} + dependencies: + '@microsoft/tsdoc': 0.14.2 + ajv: 6.12.6 + jju: 1.4.0 + resolve: 1.19.0 + dev: true + + /@microsoft/tsdoc@0.14.2: + resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} + dev: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -623,15 +669,15 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.0.2) + '@rollup/pluginutils': 5.0.2(rollup@4.0.2) rollup: 4.0.2 dev: true - /@rollup/pluginutils@5.0.5(rollup@4.0.2): - resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} + /@rollup/pluginutils@5.0.2(rollup@4.0.2): + resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} engines: {node: '>=14.0.0'} peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + rollup: ^1.20.0||^2.0.0||^3.0.0 peerDependenciesMeta: rollup: optional: true @@ -726,6 +772,40 @@ packages: requiresBuild: true optional: true + /@rushstack/node-core-library@3.61.0(@types/node@14.18.12): + resolution: {integrity: sha512-tdOjdErme+/YOu4gPed3sFS72GhtWCgNV9oDsHDnoLY5oDfwjKUc9Z+JOZZ37uAxcm/OCahDHfuu2ugqrfWAVQ==} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true + dependencies: + '@types/node': 14.18.12 + colors: 1.2.5 + fs-extra: 7.0.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.1 + semver: 7.5.4 + z-schema: 5.0.5 + dev: true + + /@rushstack/rig-package@0.5.1: + resolution: {integrity: sha512-pXRYSe29TjRw7rqxD4WS3HN/sRSbfr+tJs4a9uuaSIBAITbUggygdhuG0VrO0EO+QqH91GhYMN4S6KRtOEmGVA==} + dependencies: + resolve: 1.22.1 + strip-json-comments: 3.1.1 + dev: true + + /@rushstack/ts-command-line@4.17.1: + resolution: {integrity: sha512-2jweO1O57BYP5qdBGl6apJLB+aRIn5ccIRTPDyULh0KMwVzFqWtw6IZWt1qtUoZD/pD2RNkIOosH6Cq45rIYeg==} + dependencies: + '@types/argparse': 1.0.38 + argparse: 1.0.10 + colors: 1.2.5 + string-argv: 0.3.2 + dev: true + /@swc/core-android-arm-eabi@1.2.218: resolution: {integrity: sha512-Q/uLCh262t3xxNzhCz+ZW9t+g2nWd0gZZO4jMYFWJs7ilKVNsBfRtfnNGGACHzkVuWLNDIWtAS2PSNodl7VUHQ==} engines: {node: '>=10'} @@ -868,6 +948,10 @@ packages: '@swc/core-win32-x64-msvc': 1.2.218 dev: true + /@types/argparse@1.0.38: + resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} + dev: true + /@types/chai-subset@1.3.3: resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} dependencies: @@ -967,6 +1051,15 @@ packages: hasBin: true dev: true + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + /ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1003,7 +1096,13 @@ packages: engines: {node: '>= 8'} dependencies: normalize-path: 3.0.0 - picomatch: 2.3.1 + picomatch: 2.3.0 + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + dev: true /array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} @@ -1135,6 +1234,11 @@ packages: resolution: {integrity: sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==} dev: true + /colors@1.2.5: + resolution: {integrity: sha512-erNRLao/Y3Fv54qUa0LBB+//Uf3YwMUmdJinN20yMXm9zdKKqH9wt7R9IIVZ+K7ShzfpLV/Zg8+VyrBJYB4lpg==} + engines: {node: '>=0.1.90'} + dev: true + /commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} dev: true @@ -1143,6 +1247,13 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + /commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + requiresBuild: true + dev: true + optional: true + /concat-map@0.0.1: resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=} @@ -1509,6 +1620,10 @@ packages: signal-exit: 3.0.6 strip-final-newline: 2.0.0 + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + /fast-glob@3.2.7: resolution: {integrity: sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==} engines: {node: '>=8'} @@ -1519,6 +1634,10 @@ packages: merge2: 1.4.1 micromatch: 4.0.4 + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + /fastq@1.13.0: resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} dependencies: @@ -1544,6 +1663,15 @@ packages: universalify: 2.0.0 dev: true + /fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + dependencies: + graceful-fs: 4.2.8 + jsonfile: 4.0.0 + universalify: 0.1.2 + dev: true + /fs.realpath@1.0.0: resolution: {integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8=} @@ -1622,6 +1750,11 @@ packages: resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==} dev: true + /import-lazy@4.0.0: + resolution: {integrity: sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==} + engines: {node: '>=8'} + dev: true + /inflight@1.0.6: resolution: {integrity: sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=} dependencies: @@ -1675,6 +1808,10 @@ packages: /isexe@2.0.0: resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} + /jju@1.4.0: + resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + dev: true + /joycon@3.0.1: resolution: {integrity: sha512-SJcJNBg32dGgxhPtM0wQqxqV0ax9k/9TaUskGDSJkSFSQOEWWvQ3zzWdGQRIUry2j1zA5+ReH13t0Mf3StuVZA==} engines: {node: '>=10'} @@ -1684,6 +1821,10 @@ packages: dev: true optional: true + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + /json5@1.0.1: resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==} hasBin: true @@ -1695,6 +1836,12 @@ packages: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} dev: true + /jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + optionalDependencies: + graceful-fs: 4.2.8 + dev: true + /jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} dependencies: @@ -1719,9 +1866,21 @@ packages: engines: {node: '>=14'} dev: true + /lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + dev: true + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + dev: true + /lodash.sortby@4.7.0: resolution: {integrity: sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=} + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: true + /loupe@2.3.1: resolution: {integrity: sha512-EN1D3jyVmaX4tnajVlfbREU4axL647hLec1h/PXAb8CPDMJiYitcWF2UeLVNttRqaIqQs4x+mRvXf+d+TlDrCA==} dependencies: @@ -1734,6 +1893,13 @@ packages: get-func-name: 2.0.0 dev: true + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + /magic-string@0.27.0: resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} engines: {node: '>=12'} @@ -1760,7 +1926,7 @@ packages: engines: {node: '>=8.6'} dependencies: braces: 3.0.2 - picomatch: 2.3.1 + picomatch: 2.3.0 /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} @@ -1884,9 +2050,14 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + /picomatch@2.3.0: + resolution: {integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==} + engines: {node: '>=8.6'} + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + dev: true /pirates@4.0.1: resolution: {integrity: sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==} @@ -1974,12 +2145,19 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} dependencies: - picomatch: 2.3.1 + picomatch: 2.3.0 /resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + /resolve@1.19.0: + resolution: {integrity: sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==} + dependencies: + is-core-module: 2.9.0 + path-parse: 1.0.7 + dev: true + /resolve@1.20.0: resolution: {integrity: sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==} dependencies: @@ -2029,8 +2207,8 @@ packages: fsevents: 2.3.2 dev: true - /rollup@3.8.1: - resolution: {integrity: sha512-4yh9eMW7byOroYcN8DlF9P/2jCpu6txVIHjEqquQVSx7DI0RgyCCN3tjrcy4ra6yVtV336aLBB3v2AarYAxePQ==} + /rollup@3.25.0: + resolution: {integrity: sha512-FnJkNRst2jEZGw7f+v4hFo6UTzpDKrAKcHZWcEfm5/GJQ5CK7wgb4moNLNAe7npKUev7yQn1AY/YbZRIxOv6Qg==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: @@ -2080,6 +2258,14 @@ packages: source-map-js: 1.0.2 dev: true + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2141,6 +2327,10 @@ packages: dependencies: whatwg-url: 7.1.0 + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + dev: true + /stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true @@ -2149,6 +2339,11 @@ packages: resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==} dev: true + /string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + dev: true + /string-width@5.1.2: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} @@ -2181,6 +2376,11 @@ packages: min-indent: 1.0.1 dev: true + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + /strip-json-comments@4.0.0: resolution: {integrity: sha512-LzWcbfMbAsEDTRmhjWIioe8GcDRl0fa35YMXFoJKDdiD/quGFmjJjdgPjFJJNwCMaLyQqFIDqCdHD2V4HfLgYA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2371,7 +2571,7 @@ packages: postcss: 8.4.12 postcss-load-config: 4.0.1(postcss@8.4.12) resolve-from: 5.0.0 - rollup: 3.8.1 + rollup: 3.25.0 source-map: 0.8.0-beta.0 sucrase: 3.20.3 tree-kill: 1.2.2 @@ -2392,15 +2592,37 @@ packages: hasBin: true dev: true + /typescript@5.0.4: + resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} + engines: {node: '>=12.20'} + hasBin: true + dev: true + /ufo@1.0.1: resolution: {integrity: sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA==} dev: true + /universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + dev: true + /universalify@2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} dev: true + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.1.1 + dev: true + + /validator@13.11.0: + resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} + engines: {node: '>= 0.10'} + dev: true + /vite-node@0.28.4(sass@1.62.1)(terser@5.16.0): resolution: {integrity: sha512-KM0Q0uSG/xHHKOJvVHc5xDBabgt0l70y7/lWTR7Q0pR5/MrYxadT+y32cJOE65FfjGmJgxpVEEY+69btJgcXOQ==} engines: {node: '>=v14.16.0'} @@ -2538,6 +2760,10 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + /yaml@2.3.1: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} engines: {node: '>= 14'} @@ -2546,3 +2772,15 @@ packages: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true + + /z-schema@5.0.5: + resolution: {integrity: sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==} + engines: {node: '>=8.0.0'} + hasBin: true + dependencies: + lodash.get: 4.4.2 + lodash.isequal: 4.5.0 + validator: 13.11.0 + optionalDependencies: + commander: 9.5.0 + dev: true diff --git a/src/api-extractor.ts b/src/api-extractor.ts new file mode 100644 index 000000000..d1121d0f6 --- /dev/null +++ b/src/api-extractor.ts @@ -0,0 +1,155 @@ +import type { + ExtractorResult, + IConfigFile, + IExtractorConfigPrepareOptions, +} from '@microsoft/api-extractor' +import path from 'path' +import { handleError } from './errors' +import { + formatAggregationExports, + formatDistributionExports, + type ExportDeclaration, +} from './exports' +import { loadPkg } from './load' +import { createLogger } from './log' +import { Format, NormalizedOptions } from './options' +import { + defaultOutExtension, + ensureTempDeclarationDir, + getApiExtractor, + toAbsolutePath, + writeFileSync, +} from './utils' + +const logger = createLogger() + +function rollupDtsFile( + inputFilePath: string, + outputFilePath: string, + tsconfigFilePath: string +) { + let cwd = process.cwd() + let packageJsonFullPath = path.join(cwd, 'package.json') + let configObject: IConfigFile = { + mainEntryPointFilePath: inputFilePath, + apiReport: { + enabled: false, + + // `reportFileName` is not been used. It's just to fit the requirement of API Extractor. + reportFileName: 'tsup-report.api.md', + }, + docModel: { enabled: false }, + dtsRollup: { + enabled: true, + untrimmedFilePath: outputFilePath, + }, + tsdocMetadata: { enabled: false }, + compiler: { + tsconfigFilePath: tsconfigFilePath, + }, + projectFolder: cwd, + } + const prepareOptions: IExtractorConfigPrepareOptions = { + configObject, + configObjectFullPath: undefined, + packageJsonFullPath, + } + + const imported = getApiExtractor() + if (!imported) { + throw new Error( + `@microsoft/api-extractor is not installed. Please install it first.` + ) + } + const { ExtractorConfig, Extractor } = imported + + const extractorConfig = ExtractorConfig.prepare(prepareOptions) + + // Invoke API Extractor + const extractorResult: ExtractorResult = Extractor.invoke(extractorConfig, { + // Equivalent to the "--local" command-line parameter + localBuild: true, + + // Equivalent to the "--verbose" command-line parameter + showVerboseMessages: true, + }) + + if (!extractorResult.succeeded) { + throw new Error( + `API Extractor completed with ${extractorResult.errorCount} errors and ${extractorResult.warningCount} warnings when processing ${inputFilePath}` + ) + } +} + +async function rollupDtsFiles( + options: NormalizedOptions, + exports: ExportDeclaration[], + format: Format +) { + let declarationDir = ensureTempDeclarationDir() + let outDir = options.outDir || 'dist' + let pkg = await loadPkg(process.cwd()) + let dtsExtension = defaultOutExtension({ format, pkgType: pkg.type }).dts + + let dtsInputFilePath = path.join( + declarationDir, + '_tsup-dts-aggregation' + dtsExtension + ) + // @microsoft/api-extractor doesn't support `.d.mts` and `.d.cts` file as a + // entrypoint yet. So we replace the extension here as a temporary workaround. + // + // See the issue for more details: + // https://github.com/microsoft/rushstack/pull/4196 + dtsInputFilePath = dtsInputFilePath + .replace(/\.d\.mts$/, '.dmts.d.ts') + .replace(/\.d\.cts$/, '.dcts.d.ts') + + let dtsOutputFilePath = path.join(outDir, '_tsup-dts-rollup' + dtsExtension) + + writeFileSync( + dtsInputFilePath, + formatAggregationExports(exports, declarationDir) + ) + + rollupDtsFile( + dtsInputFilePath, + dtsOutputFilePath, + options.tsconfig || 'tsconfig.json' + ) + + for (let [out, sourceFileName] of Object.entries( + options.experimentalDts!.entry + )) { + sourceFileName = toAbsolutePath(sourceFileName) + const outFileName = path.join(outDir, out + dtsExtension) + + writeFileSync( + outFileName, + formatDistributionExports(exports, outFileName, dtsOutputFilePath) + ) + } +} + +export async function runDtsRollup( + options: NormalizedOptions, + exports?: ExportDeclaration[] +) { + try { + const start = Date.now() + const getDuration = () => { + return `${Math.floor(Date.now() - start)}ms` + } + logger.info('dts', 'Build start') + + if (!exports) { + throw new Error('Unexpected internal error: dts exports is not define') + } + for (const format of options.format) { + await rollupDtsFiles(options, exports, format) + } + logger.success('dts', `⚡️ Build success in ${getDuration()}`) + } catch (error) { + handleError(error) + logger.error('dts', 'Build error') + } +} diff --git a/src/cli-main.ts b/src/cli-main.ts index b3338b6c0..790723e19 100644 --- a/src/cli-main.ts +++ b/src/cli-main.ts @@ -38,6 +38,7 @@ export async function main(options: Options = {}) { .option('--dts [entry]', 'Generate declaration file') .option('--dts-resolve', 'Resolve externals types used for d.ts files') .option('--dts-only', 'Emit declaration files only') + .option('--experimental-dts [entry]', 'Generate declaration file (experimental)') .option( '--sourcemap [inline]', 'Generate external sourcemap, or inline source: --sourcemap inline' diff --git a/src/esbuild/postcss.ts b/src/esbuild/postcss.ts index 3c2075bcf..df9286e1d 100644 --- a/src/esbuild/postcss.ts +++ b/src/esbuild/postcss.ts @@ -1,8 +1,7 @@ -import fs from 'fs' -import path from 'path' import { Loader, Plugin, transform } from 'esbuild' -import { getPostcss } from '../utils' +import fs from 'fs' import type { Result } from 'postcss-load-config' +import { getPostcss } from '../utils' export const postcssPlugin = ({ css, @@ -104,7 +103,7 @@ export const postcssPlugin = ({ } // Transform CSS - const result = await postcss + const result = postcss ?.default(plugins) .process(contents, { ...options, from: args.path }) diff --git a/src/exports.ts b/src/exports.ts new file mode 100644 index 000000000..d265effe9 --- /dev/null +++ b/src/exports.ts @@ -0,0 +1,139 @@ +import path from 'path' +import { slash, trimDtsExtension, truthy } from './utils' + +export type ExportDeclaration = ModuleExport | NamedExport + +interface ModuleExport { + kind: 'module' + sourceFileName: string + destFileName: string + moduleName: string + isTypeOnly: boolean +} + +interface NamedExport { + kind: 'named' + sourceFileName: string + destFileName: string + alias: string + name: string + isTypeOnly: boolean +} + +export function formatAggregationExports( + exports: ExportDeclaration[], + declarationDirPath: string +): string { + const lines = exports + .map((declaration) => + formatAggregationExport(declaration, declarationDirPath) + ) + .filter(truthy) + + if (lines.length === 0) { + lines.push('export {};') + } + + return lines.join('\n') + '\n' +} + +function formatAggregationExport( + declaration: ExportDeclaration, + declarationDirPath: string +): string { + let dest = trimDtsExtension( + './' + + path.posix.normalize( + slash(path.relative(declarationDirPath, declaration.destFileName)) + ) + ) + + if (declaration.kind === 'module') { + // No implemeted + return '' + } else if (declaration.kind === 'named') { + return [ + 'export', + declaration.isTypeOnly ? 'type' : '', + '{', + declaration.name, + declaration.name === declaration.alias ? '' : `as ${declaration.alias}`, + '} from', + `'${dest}';`, + ] + .filter(truthy) + .join(' ') + } else { + throw new Error('Unknown declaration') + } +} + +export function formatDistributionExports( + exports: ExportDeclaration[], + fromFilePath: string, + toFilePath: string +) { + let importPath = trimDtsExtension( + path.posix.relative( + path.posix.dirname(path.posix.normalize(slash(fromFilePath))), + path.posix.normalize(slash(toFilePath)) + ) + ) + if (!importPath.match(/^\.+\//)) { + importPath = './' + importPath + } + + let seen = { + named: new Set(), + module: new Set(), + } + + const lines = exports + .filter((declaration) => { + if (declaration.kind === 'module') { + if (seen.module.has(declaration.moduleName)) { + return false + } + seen.module.add(declaration.moduleName) + return true + } else if (declaration.kind === 'named') { + if (seen.named.has(declaration.name)) { + return false + } + seen.named.add(declaration.name) + return true + } else { + return false + } + }) + .map((declaration) => formatDistributionExport(declaration, importPath)) + .filter(truthy) + + if (lines.length === 0) { + lines.push('export {};') + } + + return lines.join('\n') + '\n' +} + +function formatDistributionExport( + declaration: ExportDeclaration, + dest: string +): string { + if (declaration.kind === 'named') { + return [ + 'export', + declaration.isTypeOnly ? 'type' : '', + '{', + declaration.alias, + declaration.name === declaration.alias ? '' : `as ${declaration.name}`, + '} from', + `'${dest}';`, + ] + .filter(truthy) + .join(' ') + } else if (declaration.kind === 'module') { + return `export * from '${declaration.moduleName}';` + } + return '' +} diff --git a/src/index.ts b/src/index.ts index 0a542f800..247de43e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,13 @@ import path from 'path' import fs from 'fs' import { Worker } from 'worker_threads' -import { removeFiles, debouncePromise, slash, MaybePromise } from './utils' +import { + removeFiles, + debouncePromise, + slash, + MaybePromise, + toObjectEntry, +} from './utils' import { getAllDepsHash, loadTsupConfig } from './load' import glob from 'globby' import { loadTsConfig } from 'bundle-require' @@ -21,6 +27,8 @@ import { sizeReporter } from './plugins/size-reporter' import { treeShakingPlugin } from './plugins/tree-shaking' import { copyPublicDir, isInPublicDir } from './lib/public-dir' import { terserPlugin } from './plugins/terser' +import { runTypeScriptCompiler } from './tsc' +import { runDtsRollup } from './api-extractor' import { cjsInterop } from './plugins/cjs-interop' export type { Format, Options, NormalizedOptions } @@ -68,6 +76,7 @@ const normalizeOptions = async ( ...optionsFromConfigFile, ...optionsOverride, } + const options: Partial = { outDir: 'dist', ..._options, @@ -83,6 +92,20 @@ const normalizeOptions = async ( : typeof _options.dts === 'string' ? { entry: _options.dts } : _options.dts, + experimentalDts: _options.experimentalDts + ? typeof _options.experimentalDts === 'boolean' + ? _options.experimentalDts + ? { entry: {} } + : undefined + : typeof _options.experimentalDts === 'string' + ? { + entry: toObjectEntry(_options.experimentalDts), + } + : { + ..._options.experimentalDts, + entry: toObjectEntry(_options.experimentalDts.entry || {}), + } + : undefined, } setSilent(options.silent) @@ -128,6 +151,17 @@ const normalizeOptions = async ( ...(options.dts.compilerOptions || {}), } } + if (options.experimentalDts) { + options.experimentalDts.compilerOptions = { + ...(tsconfig.data.compilerOptions || {}), + ...(options.experimentalDts.compilerOptions || {}), + } + options.experimentalDts.entry = toObjectEntry( + Object.keys(options.experimentalDts.entry).length > 0 + ? options.experimentalDts.entry + : options.entry + ) + } if (!options.target) { options.target = tsconfig.data?.compilerOptions?.target?.toLowerCase() } @@ -173,6 +207,17 @@ export async function build(_options: Options) { } const dtsTask = async () => { + if (options.dts && options.experimentalDts) { + throw new Error( + "You can't use both `dts` and `experimentalDts` at the same time" + ) + } + + if (options.experimentalDts) { + const exports = runTypeScriptCompiler(options) + await runDtsRollup(options, exports) + } + if (options.dts) { await new Promise((resolve, reject) => { const worker = new Worker(path.join(__dirname, './rollup.js')) diff --git a/src/options.ts b/src/options.ts index 884dfa449..0921abdae 100644 --- a/src/options.ts +++ b/src/options.ts @@ -1,9 +1,9 @@ import type { BuildOptions, Plugin as EsbuildPlugin, Loader } from 'esbuild' import type { InputOption } from 'rollup' +import type { MinifyOptions } from 'terser' import { MarkRequired } from 'ts-essentials' import type { Plugin } from './plugin' import type { TreeshakingStrategy } from './plugins/tree-shaking' -import type { MinifyOptions } from 'terser' export type KILL_SIGNAL = 'SIGKILL' | 'SIGTERM' @@ -39,6 +39,15 @@ export type DtsConfig = { compilerOptions?: any } +export type ExperimentalDtsConfig = { + entry?: InputOption + /** + * Overrides `compilerOptions` + * This option takes higher priority than `compilerOptions` in tsconfig.json + */ + compilerOptions?: any +} + export type BannerOrFooter = | { js?: string @@ -126,6 +135,7 @@ export type Options = { [k: string]: string } dts?: boolean | string | DtsConfig + experimentalDts?: boolean | string | ExperimentalDtsConfig sourcemap?: boolean | 'inline' /** Always bundle modules matching given patterns */ noExternal?: (string | RegExp)[] @@ -139,7 +149,7 @@ export type Options = { /** * Code splitting * Default to `true` for ESM, `false` for CJS. - * + * * You can set it to `true` explicitly, and may want to disable code splitting sometimes: [`#255`](https://github.com/egoist/tsup/issues/255) */ splitting?: boolean @@ -232,11 +242,17 @@ export type Options = { cjsInterop?: boolean } +export interface NormalizedExperimentalDtsConfig { + entry: { [entryAlias: string]: string } + compilerOptions?: any +} + export type NormalizedOptions = Omit< MarkRequired, - 'dts' | 'format' + 'dts' | 'experimentalDts' | 'format' > & { dts?: DtsConfig + experimentalDts?: NormalizedExperimentalDtsConfig tsconfigResolvePaths: Record tsconfigDecoratorMetadata?: boolean format: Format[] diff --git a/src/rollup.ts b/src/rollup.ts index 35fdea1e1..8691a3883 100644 --- a/src/rollup.ts +++ b/src/rollup.ts @@ -4,7 +4,7 @@ import { NormalizedOptions } from './' import ts from 'typescript' import jsonPlugin from '@rollup/plugin-json' import { handleError } from './errors' -import { defaultOutExtension, removeFiles } from './utils' +import { defaultOutExtension, removeFiles, toObjectEntry } from './utils' import { TsResolveOptions, tsResolvePlugin } from './rollup/ts-resolve' import { createLogger, setSilent } from './log' import { getProductionDeps, loadPkg } from './load' @@ -34,48 +34,6 @@ type RollupConfig = { outputConfig: OutputOptions[] } -const findLowestCommonAncestor = (filepaths: string[]) => { - if (filepaths.length <= 1) return '' - const [first, ...rest] = filepaths - let ancestor = first.split('/') - for (const filepath of rest) { - const directories = filepath.split('/', ancestor.length) - let index = 0 - for (const directory of directories) { - if (directory === ancestor[index]) { - index += 1 - } else { - ancestor = ancestor.slice(0, index) - break - } - } - ancestor = ancestor.slice(0, index) - } - - return ancestor.length <= 1 && ancestor[0] === '' - ? '/' + ancestor[0] - : ancestor.join('/') -} - -// Make sure the Rollup entry is an object -// We use the base path (without extension) as the entry name -// To make declaration files work with multiple entrypoints -// See #316 -const toObjectEntry = (entry: string[]) => { - entry = entry.map((e) => e.replace(/\\/g, '/')) - const ancestor = findLowestCommonAncestor(entry) - return entry.reduce((result, item) => { - const key = item - .replace(ancestor, '') - .replace(/^\//, '') - .replace(/\.[a-z]+$/, '') - return { - ...result, - [key]: item, - } - }, {}) -} - const getRollupConfig = async ( options: NormalizedOptions ): Promise => { diff --git a/src/tsc.ts b/src/tsc.ts new file mode 100644 index 000000000..9094dd768 --- /dev/null +++ b/src/tsc.ts @@ -0,0 +1,218 @@ +import { loadTsConfig } from 'bundle-require' +import ts from 'typescript' +import { handleError } from './errors' +import { ExportDeclaration } from './exports' +import { createLogger } from './log' +import { NormalizedOptions } from './options' +import { ensureTempDeclarationDir, toAbsolutePath } from './utils' +import { dirname } from 'path' + +const logger = createLogger() + +class AliasPool { + private seen = new Set() + + assign(name: string): string { + let suffix = 0 + let alias = name === 'default' ? 'default_alias' : name + + while (this.seen.has(alias)) { + alias = `${name}_alias_${++suffix}` + if (suffix >= 1000) { + throw new Error( + 'Alias generation exceeded limit. Possible infinite loop detected.' + ) + } + } + + this.seen.add(alias) + return alias + } +} + +/** + * Get all export declarations from root files. + */ +function getExports( + program: ts.Program, + fileMapping: Map +): ExportDeclaration[] { + let checker = program.getTypeChecker() + let aliasPool = new AliasPool() + let assignAlias = aliasPool.assign.bind(aliasPool) + + function extractExports(sourceFileName: string): ExportDeclaration[] { + const cwd = program.getCurrentDirectory() + sourceFileName = toAbsolutePath(sourceFileName, cwd) + + const sourceFile = program.getSourceFile(sourceFileName) + if (!sourceFile) { + return [] + } + + const destFileName = fileMapping.get(sourceFileName) + if (!destFileName) { + return [] + } + + const moduleSymbol = checker.getSymbolAtLocation(sourceFile) + if (!moduleSymbol) { + return [] + } + + const exports: ExportDeclaration[] = [] + + const exportSymbols = checker.getExportsOfModule(moduleSymbol) + exportSymbols.forEach((symbol) => { + const name = symbol.getName() + exports.push({ + kind: 'named', + sourceFileName, + destFileName, + name, + alias: assignAlias(name), + isTypeOnly: false, + }) + }) + + return exports + } + + return program.getRootFileNames().flatMap(extractExports) +} + +/** + * Use TypeScript compiler to emit declaration files. + * + * @returns The mapping from source TS file paths to output declaration file paths + */ +function emitDtsFiles(program: ts.Program, host: ts.CompilerHost) { + let fileMapping = new Map() + + let writeFile: ts.WriteFileCallback = ( + fileName, + text, + writeByteOrderMark, + onError, + sourceFiles, + data + ) => { + const sourceFile = sourceFiles?.[0] + let sourceFileName = sourceFile?.fileName + + if (sourceFileName && !fileName.endsWith('.map')) { + const cwd = program.getCurrentDirectory() + fileMapping.set( + toAbsolutePath(sourceFileName, cwd), + toAbsolutePath(fileName, cwd) + ) + } + + return host.writeFile( + fileName, + text, + writeByteOrderMark, + onError, + sourceFiles, + data + ) + } + + let emitResult = program.emit(undefined, writeFile, undefined, true) + + let diagnostics = ts + .getPreEmitDiagnostics(program) + .concat(emitResult.diagnostics) + + let diagnosticMessages: string[] = [] + + diagnostics.forEach((diagnostic) => { + if (diagnostic.file) { + let { line, character } = ts.getLineAndCharacterOfPosition( + diagnostic.file, + diagnostic.start! + ) + let message = ts.flattenDiagnosticMessageText( + diagnostic.messageText, + '\n' + ) + diagnosticMessages.push( + `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}` + ) + } else { + let message = ts.flattenDiagnosticMessageText( + diagnostic.messageText, + '\n' + ) + diagnosticMessages.push(message) + } + }) + + let diagnosticMessage = diagnosticMessages.join('\n') + if (diagnosticMessage) { + logger.error( + 'TSC', + 'Failed to emit declaration files.\n\n' + diagnosticMessage + ) + throw new Error('TypeScript compilation failed') + } + + return fileMapping +} + +function emit(compilerOptions?: any, tsconfig?: string) { + let cwd = process.cwd() + let rawTsconfig = loadTsConfig(cwd, tsconfig) + if (!rawTsconfig) { + throw new Error(`Unable to find ${tsconfig || 'tsconfig.json'} in ${cwd}`) + } + + let declarationDir = ensureTempDeclarationDir() + + let parsedTsconfig = ts.parseJsonConfigFileContent( + { + ...rawTsconfig.data, + compilerOptions: { + ...rawTsconfig.data?.compilerOptions, + + // Enable declaration emit and disable javascript emit + noEmit: false, + declaration: true, + declarationMap: true, + declarationDir: declarationDir, + emitDeclarationOnly: true, + }, + }, + ts.sys, + tsconfig ? dirname(tsconfig) : './' + ) + + let options: ts.CompilerOptions = parsedTsconfig.options + + let host: ts.CompilerHost = ts.createCompilerHost(options) + let program: ts.Program = ts.createProgram( + parsedTsconfig.fileNames, + options, + host + ) + + let fileMapping = emitDtsFiles(program, host) + return getExports(program, fileMapping) +} + +export function runTypeScriptCompiler(options: NormalizedOptions) { + try { + const start = Date.now() + const getDuration = () => { + return `${Math.floor(Date.now() - start)}ms` + } + logger.info('tsc', 'Build start') + const dtsOptions = options.experimentalDts! + const exports = emit(dtsOptions.compilerOptions, options.tsconfig) + logger.success('tsc', `⚡️ Build success in ${getDuration()}`) + return exports + } catch (error) { + handleError(error) + logger.error('tsc', 'Build error') + } +} diff --git a/src/utils.ts b/src/utils.ts index ad1202b3f..cc7f6b7ca 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,8 +1,9 @@ import fs from 'fs' import glob from 'globby' +import path from 'path' import resolveFrom from 'resolve-from' import strip from 'strip-json-comments' -import { Format } from './options' +import { Entry, Format } from './options' export type MaybePromise = T | Promise @@ -44,9 +45,14 @@ export function isExternal( return false } -export function getPostcss(): null | typeof import('postcss') { - const p = resolveFrom.silent(process.cwd(), 'postcss') - return p && require(p) +export function getPostcss(): null | Awaited { + return localRequire('postcss') +} + +export function getApiExtractor(): null | Awaited< + typeof import('@microsoft/api-extractor') +> { + return localRequire('@microsoft/api-extractor') } export function localRequire(moduleName: string) { @@ -137,7 +143,7 @@ export function defaultOutExtension({ }: { format: Format pkgType?: string -}): { js: string, dts: string } { +}): { js: string; dts: string } { let jsExtension = '.js' let dtsExtension = '.d.ts' const isModule = pkgType === 'module' @@ -157,3 +163,84 @@ export function defaultOutExtension({ dts: dtsExtension, } } + +export function ensureTempDeclarationDir(): string { + const cwd = process.cwd() + const dirPath = path.join(cwd, '.tsup', 'declaration') + + if (fs.existsSync(dirPath)) { + return dirPath + } + + fs.mkdirSync(dirPath, { recursive: true }) + + const gitIgnorePath = path.join(cwd, '.tsup', '.gitignore') + writeFileSync(gitIgnorePath, '**/*\n') + + return dirPath +} + +// Make sure the entry is an object +// We use the base path (without extension) as the entry name +// To make declaration files work with multiple entrypoints +// See #316 +export const toObjectEntry = (entry: string | Entry) => { + if (typeof entry === 'string') { + entry = [entry] + } + if (!Array.isArray(entry)) { + return entry + } + entry = entry.map((e) => e.replace(/\\/g, '/')) + const ancestor = findLowestCommonAncestor(entry) + return entry.reduce((result, item) => { + const key = item + .replace(ancestor, '') + .replace(/^\//, '') + .replace(/\.[a-z]+$/, '') + return { + ...result, + [key]: item, + } + }, {} as Record) +} + +const findLowestCommonAncestor = (filepaths: string[]) => { + if (filepaths.length <= 1) return '' + const [first, ...rest] = filepaths + let ancestor = first.split('/') + for (const filepath of rest) { + const directories = filepath.split('/', ancestor.length) + let index = 0 + for (const directory of directories) { + if (directory === ancestor[index]) { + index += 1 + } else { + ancestor = ancestor.slice(0, index) + break + } + } + ancestor = ancestor.slice(0, index) + } + + return ancestor.length <= 1 && ancestor[0] === '' + ? '/' + ancestor[0] + : ancestor.join('/') +} + +export function toAbsolutePath(p: string, cwd?: string): string { + if (path.isAbsolute(p)) { + return p + } + + return slash(path.normalize(path.join(cwd || process.cwd(), p))) +} + +export function trimDtsExtension(fileName: string) { + return fileName.replace(/\.d\.(ts|mts|cts)x?$/, '') +} + +export function writeFileSync(filePath: string, content: string) { + fs.mkdirSync(path.dirname(filePath), { recursive: true }) + fs.writeFileSync(filePath, content) +} diff --git a/test/__snapshots__/index.test.ts.snap b/test/__snapshots__/index.test.ts.snap index 554afdb20..6f0b102dc 100644 --- a/test/__snapshots__/index.test.ts.snap +++ b/test/__snapshots__/index.test.ts.snap @@ -180,6 +180,295 @@ export { stuff }; " `; +exports[`should emit declaration files with experimentalDts 1`] = ` +" +////////////////////////////////////////////////////////////////////// +// dist/_tsup-dts-rollup.d.mts +////////////////////////////////////////////////////////////////////// + +import { renderToNodeStream } from 'react-dom/server'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { renderToStaticNodeStream } from 'react-dom/server'; +import { renderToString } from 'react-dom/server'; +import * as ServerThirdPartyNamespace from 'react-dom'; +import { version } from 'react-dom/server'; + +declare interface ClientRenderOptions { + document: boolean; +} +export { ClientRenderOptions } +export { ClientRenderOptions as ClientRenderOptions_alias_1 } + +export declare function default_alias(options: ServerRenderOptions): void; + +declare function render(options: ClientRenderOptions): string; +export { render } +export { render as render_alias_1 } + +/** + * Comment for server render function + */ +export declare function render_alias_2(options: ServerRenderOptions): string; + +export { renderToNodeStream } + +export { renderToStaticMarkup } + +export { renderToStaticNodeStream } + +export { renderToString } + +export declare class ServerClass { +} + +declare const serverConstant = 1; +export { serverConstant } +export { serverConstant as serverConstantAlias } + +export declare interface ServerRenderOptions { + /** + * Comment for ServerRenderOptions.stream + * + * @public + * + * @my_custom_tag + */ + stream: boolean; +} + +export { ServerThirdPartyNamespace } + +declare function sharedFunction(value: T): T | null; +export { sharedFunction } +export { sharedFunction as sharedFunction_alias_1 } +export { sharedFunction as sharedFunction_alias_2 } +export { sharedFunction as sharedFunction_alias_3 } + +declare type sharedType = { + shared: boolean; +}; +export { sharedType } +export { sharedType as sharedType_alias_1 } +export { sharedType as sharedType_alias_2 } +export { sharedType as sharedType_alias_3 } + +export declare const VERSION: \\"0.0.0\\"; + +export { version } + +export { } + + +////////////////////////////////////////////////////////////////////// +// dist/_tsup-dts-rollup.d.ts +////////////////////////////////////////////////////////////////////// + +import { renderToNodeStream } from 'react-dom/server'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { renderToStaticNodeStream } from 'react-dom/server'; +import { renderToString } from 'react-dom/server'; +import * as ServerThirdPartyNamespace from 'react-dom'; +import { version } from 'react-dom/server'; + +declare interface ClientRenderOptions { + document: boolean; +} +export { ClientRenderOptions } +export { ClientRenderOptions as ClientRenderOptions_alias_1 } + +export declare function default_alias(options: ServerRenderOptions): void; + +declare function render(options: ClientRenderOptions): string; +export { render } +export { render as render_alias_1 } + +/** + * Comment for server render function + */ +export declare function render_alias_2(options: ServerRenderOptions): string; + +export { renderToNodeStream } + +export { renderToStaticMarkup } + +export { renderToStaticNodeStream } + +export { renderToString } + +export declare class ServerClass { +} + +declare const serverConstant = 1; +export { serverConstant } +export { serverConstant as serverConstantAlias } + +export declare interface ServerRenderOptions { + /** + * Comment for ServerRenderOptions.stream + * + * @public + * + * @my_custom_tag + */ + stream: boolean; +} + +export { ServerThirdPartyNamespace } + +declare function sharedFunction(value: T): T | null; +export { sharedFunction } +export { sharedFunction as sharedFunction_alias_1 } +export { sharedFunction as sharedFunction_alias_2 } +export { sharedFunction as sharedFunction_alias_3 } + +declare type sharedType = { + shared: boolean; +}; +export { sharedType } +export { sharedType as sharedType_alias_1 } +export { sharedType as sharedType_alias_2 } +export { sharedType as sharedType_alias_3 } + +export declare const VERSION: \\"0.0.0\\"; + +export { version } + +export { } + + +////////////////////////////////////////////////////////////////////// +// dist/index.d.mts +////////////////////////////////////////////////////////////////////// + +export { render } from './_tsup-dts-rollup'; +export { ClientRenderOptions } from './_tsup-dts-rollup'; +export { sharedFunction } from './_tsup-dts-rollup'; +export { sharedType } from './_tsup-dts-rollup'; +export { VERSION } from './_tsup-dts-rollup'; +export { default_alias as default } from './_tsup-dts-rollup'; +export { ServerRenderOptions } from './_tsup-dts-rollup'; +export { serverConstant } from './_tsup-dts-rollup'; +export { serverConstantAlias } from './_tsup-dts-rollup'; +export { ServerClass } from './_tsup-dts-rollup'; +export { ServerThirdPartyNamespace } from './_tsup-dts-rollup'; +export { renderToString } from './_tsup-dts-rollup'; +export { renderToNodeStream } from './_tsup-dts-rollup'; +export { renderToStaticMarkup } from './_tsup-dts-rollup'; +export { renderToStaticNodeStream } from './_tsup-dts-rollup'; +export { version } from './_tsup-dts-rollup'; + + +////////////////////////////////////////////////////////////////////// +// dist/index.d.ts +////////////////////////////////////////////////////////////////////// + +export { render } from './_tsup-dts-rollup'; +export { ClientRenderOptions } from './_tsup-dts-rollup'; +export { sharedFunction } from './_tsup-dts-rollup'; +export { sharedType } from './_tsup-dts-rollup'; +export { VERSION } from './_tsup-dts-rollup'; +export { default_alias as default } from './_tsup-dts-rollup'; +export { ServerRenderOptions } from './_tsup-dts-rollup'; +export { serverConstant } from './_tsup-dts-rollup'; +export { serverConstantAlias } from './_tsup-dts-rollup'; +export { ServerClass } from './_tsup-dts-rollup'; +export { ServerThirdPartyNamespace } from './_tsup-dts-rollup'; +export { renderToString } from './_tsup-dts-rollup'; +export { renderToNodeStream } from './_tsup-dts-rollup'; +export { renderToStaticMarkup } from './_tsup-dts-rollup'; +export { renderToStaticNodeStream } from './_tsup-dts-rollup'; +export { version } from './_tsup-dts-rollup'; + + +////////////////////////////////////////////////////////////////////// +// dist/my-lib-client.d.mts +////////////////////////////////////////////////////////////////////// + +export { render } from './_tsup-dts-rollup'; +export { ClientRenderOptions } from './_tsup-dts-rollup'; +export { sharedFunction } from './_tsup-dts-rollup'; +export { sharedType } from './_tsup-dts-rollup'; +export { VERSION } from './_tsup-dts-rollup'; +export { default_alias as default } from './_tsup-dts-rollup'; +export { ServerRenderOptions } from './_tsup-dts-rollup'; +export { serverConstant } from './_tsup-dts-rollup'; +export { serverConstantAlias } from './_tsup-dts-rollup'; +export { ServerClass } from './_tsup-dts-rollup'; +export { ServerThirdPartyNamespace } from './_tsup-dts-rollup'; +export { renderToString } from './_tsup-dts-rollup'; +export { renderToNodeStream } from './_tsup-dts-rollup'; +export { renderToStaticMarkup } from './_tsup-dts-rollup'; +export { renderToStaticNodeStream } from './_tsup-dts-rollup'; +export { version } from './_tsup-dts-rollup'; + + +////////////////////////////////////////////////////////////////////// +// dist/my-lib-client.d.ts +////////////////////////////////////////////////////////////////////// + +export { render } from './_tsup-dts-rollup'; +export { ClientRenderOptions } from './_tsup-dts-rollup'; +export { sharedFunction } from './_tsup-dts-rollup'; +export { sharedType } from './_tsup-dts-rollup'; +export { VERSION } from './_tsup-dts-rollup'; +export { default_alias as default } from './_tsup-dts-rollup'; +export { ServerRenderOptions } from './_tsup-dts-rollup'; +export { serverConstant } from './_tsup-dts-rollup'; +export { serverConstantAlias } from './_tsup-dts-rollup'; +export { ServerClass } from './_tsup-dts-rollup'; +export { ServerThirdPartyNamespace } from './_tsup-dts-rollup'; +export { renderToString } from './_tsup-dts-rollup'; +export { renderToNodeStream } from './_tsup-dts-rollup'; +export { renderToStaticMarkup } from './_tsup-dts-rollup'; +export { renderToStaticNodeStream } from './_tsup-dts-rollup'; +export { version } from './_tsup-dts-rollup'; + + +////////////////////////////////////////////////////////////////////// +// dist/server/index.d.mts +////////////////////////////////////////////////////////////////////// + +export { render } from '../_tsup-dts-rollup'; +export { ClientRenderOptions } from '../_tsup-dts-rollup'; +export { sharedFunction } from '../_tsup-dts-rollup'; +export { sharedType } from '../_tsup-dts-rollup'; +export { VERSION } from '../_tsup-dts-rollup'; +export { default_alias as default } from '../_tsup-dts-rollup'; +export { ServerRenderOptions } from '../_tsup-dts-rollup'; +export { serverConstant } from '../_tsup-dts-rollup'; +export { serverConstantAlias } from '../_tsup-dts-rollup'; +export { ServerClass } from '../_tsup-dts-rollup'; +export { ServerThirdPartyNamespace } from '../_tsup-dts-rollup'; +export { renderToString } from '../_tsup-dts-rollup'; +export { renderToNodeStream } from '../_tsup-dts-rollup'; +export { renderToStaticMarkup } from '../_tsup-dts-rollup'; +export { renderToStaticNodeStream } from '../_tsup-dts-rollup'; +export { version } from '../_tsup-dts-rollup'; + + +////////////////////////////////////////////////////////////////////// +// dist/server/index.d.ts +////////////////////////////////////////////////////////////////////// + +export { render } from '../_tsup-dts-rollup'; +export { ClientRenderOptions } from '../_tsup-dts-rollup'; +export { sharedFunction } from '../_tsup-dts-rollup'; +export { sharedType } from '../_tsup-dts-rollup'; +export { VERSION } from '../_tsup-dts-rollup'; +export { default_alias as default } from '../_tsup-dts-rollup'; +export { ServerRenderOptions } from '../_tsup-dts-rollup'; +export { serverConstant } from '../_tsup-dts-rollup'; +export { serverConstantAlias } from '../_tsup-dts-rollup'; +export { ServerClass } from '../_tsup-dts-rollup'; +export { ServerThirdPartyNamespace } from '../_tsup-dts-rollup'; +export { renderToString } from '../_tsup-dts-rollup'; +export { renderToNodeStream } from '../_tsup-dts-rollup'; +export { renderToStaticMarkup } from '../_tsup-dts-rollup'; +export { renderToStaticNodeStream } from '../_tsup-dts-rollup'; +export { version } from '../_tsup-dts-rollup'; +" +`; + exports[`simple 1`] = ` "\\"use strict\\"; var __defProp = Object.defineProperty; diff --git a/test/index.test.ts b/test/index.test.ts index fcbc17828..0228d643a 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -5,7 +5,7 @@ import fs from 'fs-extra' import glob from 'globby' import waitForExpect from 'wait-for-expect' import { fileURLToPath } from 'url' -import { debouncePromise } from '../src/utils' +import { debouncePromise, slash } from '../src/utils' const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -1345,9 +1345,14 @@ test('should emit a declaration file per format', async () => { format: ['esm', 'cjs'], dts: true }`, - }); - expect(outFiles).toEqual(['input.d.mts', 'input.d.ts', 'input.js', 'input.mjs']) -}); + }) + expect(outFiles).toEqual([ + 'input.d.mts', + 'input.d.ts', + 'input.js', + 'input.mjs', + ]) +}) test('should emit a declaration file per format (type: module)', async () => { const { outFiles } = await run(getTestName(), { @@ -1361,6 +1366,160 @@ test('should emit a declaration file per format (type: module)', async () => { format: ['esm', 'cjs'], dts: true }`, - }); - expect(outFiles).toEqual(['input.cjs', 'input.d.cts', 'input.d.ts', 'input.js']) -}); + }) + expect(outFiles).toEqual([ + 'input.cjs', + 'input.d.cts', + 'input.d.ts', + 'input.js', + ]) +}) + +test('should emit declaration files with experimentalDts', async () => { + const files = { + 'package.json': ` + { + "name": "tsup-playground", + "private": true, + "version": "0.0.0", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.mjs", + "default": "./dist/index.js" + }, + "./client": { + "types": "./dist/my-lib-client.d.ts", + "require": "./dist/my-lib-client.js", + "import": "./dist/my-lib-client.mjs", + "default": "./dist/my-lib-client.js" + }, + "./server": { + "types": "./dist/server/index.d.ts", + "require": "./dist/server/index.js", + "import": "./dist/server/index.mjs", + "default": "./dist/server/index.js" + } + } + } + `, + 'tsconfig.json': ` + { + "compilerOptions": { + "target": "ES2020", + "skipLibCheck": true, + "noEmit": true + }, + "include": ["./src"] + } + `, + 'tsup.config.ts': ` + export default { + name: 'tsup', + target: 'es2022', + format: [ + 'esm', + 'cjs' + ], + entry: { + index: './src/index.ts', + 'my-lib-client': './src/client.ts', + 'server/index': './src/server.ts', + }, + } + `, + 'src/shared.ts': ` + export function sharedFunction(value: T): T | null { + return value || null + } + + type sharedType = { + shared: boolean + } + + export type { sharedType } + `, + 'src/server.ts': ` + export * from './shared' + + /** + * Comment for server render function + */ + export function render(options: ServerRenderOptions): string { + return JSON.stringify(options) + } + + export interface ServerRenderOptions { + /** + * Comment for ServerRenderOptions.stream + * + * @public + * + * @my_custom_tag + */ + stream: boolean + } + + export const serverConstant = 1 + + export { serverConstant as serverConstantAlias } + + export class ServerClass {}; + + export default function serverDefaultExport(options: ServerRenderOptions): void {}; + + // Export a third party module as a namespace + import * as ServerThirdPartyNamespace from 'react-dom'; + export { ServerThirdPartyNamespace } + + // Export a third party module + export * from 'react-dom/server'; + + `, + 'src/client.ts': ` + export * from './shared' + + export function render(options: ClientRenderOptions): string { + return JSON.stringify(options) + } + + export interface ClientRenderOptions { + document: boolean + } + `, + 'src/index.ts': ` + export * from './client' + export * from './shared' + + export const VERSION = '0.0.0' as const + `, + } + const { outFiles, getFileContent } = await run(getTestName(), files, { + entry: [], + flags: ['--experimental-dts'], + }) + const snapshots: string[] = [] + await Promise.all( + outFiles + .filter((outFile) => outFile.includes('.d.')) + .map(async (outFile) => { + const filePath = path.join('dist', outFile) + const content = await getFileContent(filePath) + snapshots.push( + [ + '', + '/'.repeat(70), + `// ${path.posix.normalize(slash(filePath))}`, + '/'.repeat(70), + '', + content, + ].join('\n') + ) + }) + ) + expect(snapshots.sort().join('\n')).toMatchSnapshot() +})