diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b043266 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: [push, pull_request] + +jobs: + build: + strategy: + matrix: + node-version: [16.x, 18.x, 19.x] + platform: + - os: ubuntu-latest + shell: bash + - os: macos-latest + shell: bash + - os: windows-latest + shell: bash + - os: windows-latest + shell: powershell + fail-fast: false + + runs-on: ${{ matrix.platform.os }} + defaults: + run: + shell: ${{ matrix.platform.shell }} + + steps: + - name: Checkout Repository + uses: actions/checkout@v1.1.0 + + - name: Use Nodejs ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Run Tests + run: npm test -- -c -t0 diff --git a/.github/workflows/typedoc.yml b/.github/workflows/typedoc.yml new file mode 100644 index 0000000..e5bc0ef --- /dev/null +++ b/.github/workflows/typedoc.yml @@ -0,0 +1,50 @@ +# Simple workflow for deploying static content to GitHub Pages +name: Deploy static content to Pages + +on: + # Runs on pushes targeting the default branch + push: + branches: ["main"] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow one concurrent deployment +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + # Single deploy job since we're just deploying + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Use Nodejs ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: 18.x + - name: Install dependencies + run: npm install + - name: Generate typedocs + run: npm run typedoc + + - name: Setup Pages + uses: actions/configure-pages@v3 + - name: Upload artifact + uses: actions/upload-pages-artifact@v1 + with: + path: './docs' + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42cdaf6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.tshy* +/node_modules +/dist diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..881248b --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,63 @@ +All packages under `src/` are licensed according to the terms in +their respective `LICENSE` or `LICENSE.md` files. + +The remainder of this project is licensed under the Blue Oak +Model License, as follows: + +----- + +# Blue Oak Model License + +Version 1.0.0 + +## Purpose + +This license gives everyone as much permission to work with +this software as possible, while protecting contributors +from liability. + +## Acceptance + +In order to receive this license, you must agree to its +rules. The rules of this license are both obligations +under that agreement and conditions to your license. +You must not do anything with this software that triggers +a rule that you cannot or will not follow. + +## Copyright + +Each contributor licenses you to do everything with this +software that would otherwise infringe that contributor's +copyright in it. + +## Notices + +You must ensure that everyone who gets a copy of +any part of this software from you, with or without +changes, also gets the text of this license or a link to +. + +## Excuse + +If anyone notifies you in writing that you have not +complied with [Notices](#notices), you can keep your +license by taking all practical steps to comply within 30 +days after the notice. If you do not do so, your license +ends immediately. + +## Patent + +Each contributor licenses you to do everything with this +software that would otherwise infringe any patent claims +they can license or become able to license. + +## Reliability + +No contributor can revoke this license. + +## No Liability + +***As far as the law allows, this software comes as is, +without any warranty or condition, and no contributor +will be liable to anyone for any damages related to this +software or this license, under any kind of legal claim.*** diff --git a/README.md b/README.md new file mode 100644 index 0000000..016e149 --- /dev/null +++ b/README.md @@ -0,0 +1,235 @@ +# tshy - TypeScript HYbridizer + +Hybrid (CommonJS/ESM) TypeScript node package builder. + +This tool manages the `exports` in your package.json file, and +builds your TypeScript program using `tsc` 5.2 in both ESM and +CJS modes. + +## USAGE + +Install tshy: + +``` +npm i -D tshy +``` + +Put this in your package.json to use it with the default configs: + +```json +{ + "files": [ + "dist" + ], + "scripts": { + "prepare": "tshy" + } +} +``` + +Put your source code in `./src`. + +The built files will end up in `./dist/esm` (ESM) and +`./dist/commonjs` (CommonJS). + +Your `exports` will be edited to reflect the correct module entry +points. + +## Configuration + +Mostly, this is opinionated convention, and so there is very +little to configure. + +Source must be in `./src`. Builds are in `./dist/cjs` for +CommonJS and `./dist/mjs` for ESM. + +There is very little configuration for this. The only thing to +decide is the exported paths. If you have a `./index.ts` file, +then that will be listed as the main `"."` export by default. + +You can set other entry points by putting this in your +`package.json` file: + +```json +{ + "tshy": { + "exports": { + "./foo": "./src/foo.ts", + "./bar": "./src/bar.ts", + ".": "./src/something-other-than-index.ts", + "./package.json": "./package.json" + } + } +} +``` + +Any exports pointing to files in `./src` will be updated to their +appropriate build target locations, like: + +```json +{ + "exports": { + "./foo": { + "import": { + "types": "./dist/mjs/foo.d.ts", + "default": "./dist/mjs/foo.js" + }, + "require": { + "types": "./dist/cjs/foo.d.ts", + "default": "./dist/cjs/foo.js" + } + } + } +} +``` + +Any exports that are not within `./src` will not be built, and +can be either a string, or a `{ import, require, types }` object: + +```json +{ + "exports": { + "./package.json": "./package.json" + "./thing": { + "import": "./lib/thing.mjs", + "require": "./lib/thing.cjs", + "types": "./lib/thing.d.ts" + } + } +} +``` + +## Selecting Dialects + +You can tell tshy which dialect you're building for by setting +the `dialects` config to an array of strings: + +```json +{ + "tshy": { + "dialects": [ + "esm", + "commonjs" + ] + } +} +``` + +The default is `["esm", "commonjs"]` (ie, both of them). If you +set it to just one, then only that dialect will be built and +exported. + +## CommonJS Dialect Polyfills + +Sometimes you have to do something in different ways depending on +the JS dialect in use. For example, maybe you have to use +`import.meta.url` in ESM, but polyfill with +`pathToFileURL(__filename)` in CommonJS. + +To do this, create a polyfill file with the CommonJS code in +`-cjs.cts`. (The `cts` extension matters.) + +```js +// src/source-dir-cjs.cts +// ^^^^^^^^^^--------- matching name +// ^^^^----- "-cts" tag +// ^^^^- ".cts" filename suffix +// this one has a -cjs.cts suffix, so it will override the +// module at src/source-dir.ts in the CJS build, +// and be excluded from the esm build. +import { pathToFileURL } from 'node:url' +//@ts-ignore - Have to ignore because TSC thinks this is ESM +export const sourceDir = pathToFileURL(__dirname) +``` + +Then put the "real" ESM code in `.ts` (not `.mts`!) + +```js +// src/source-dir.ts +// This is the ESM version of the module +export const sourceDir = new URL('.', import.meta.url) +``` + +Then in your code, you can just `import { sourceDir } from +'./source-dir.js'` and it'll work in both dialects. + +## `.cts` and `.mts` files + +Files named `*.mts` will be excluded from the CommonJS build. + +Files named `*.cts` will be excluded from the ESM build. + +If you need to do something one way for CJS and another way for +ESM, use the "Dialect Switching" trick, with the ESM code living +in `src/.ts` and the CommonJS polyfill living in +`src/-cjs.cts`. + +## Atomic Builds + +Code is built in `./.tshy-build-tmp` and then copied over only if +the build succeeds. This makes it work in monorepo cases where +you may have packages that depend on one another and are all +being built in parallel (as long as they've been built one time, +of course). + +## Exports Management + +The `exports` field in your package.json file will be updated +based on the `tshy.exports` configuration, as described above. + +If you don't provide that config, then the default is: + +```json +{ + "tshy": { + "exports": { + ".": "./src/index.ts", + "./package.json": "./package.json" + } + } +} +``` + +## Package `#imports` + +Using the `imports` field in `package.json` is not currently +supported, because this looks at the nearest `package.json` to +get local imports, and the package.json files placed in +`dist/{cjs,mjs}` can't have local imports outside of their +folders. + +There's a way it could theoretically be done, but it's a bit +complicated. A future version may support this. + +## TSConfigs + +Put whatever configuration you want in `tsconfig.json`, with the +following caveats: + +* `include` - will be overridden based on build, best omitted +* `exclude` - will be overridden based on build, best omitted +* compilerOptions: + * `outDir` - will be overridden based on build, best omitted + * `rootDir` - will be set to `./src` in the build, can only + cause annoying errors otherwise. + * `target` - will be set to `es2022` + * `module` - will be set to `NodeNext` + * `moduleResolution` - will be set to `NodeNext` + +If you don't have a `tsconfig.json` file, then one will be +provided for you. + +Then the `tsconfig.json` file will be used as the default project +for code hints in VSCode/nvim, your tests, etc. + +## `src/package.json` + +As of TypeScript 5.2, the only way to emit JavaScript to ESM or +CJS, and also import packages using node-style `"exports"`-aware +module resolution, is to set the `type` field in the +`package.json` file closest to the TypeScript source code. + +During the build, `tshy` will create a file at `src/package.json` +for this purpose, and then delete it afterwards. If that file +exists and _wasn't_ put there by `tshy`, then it will be +destroyed. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0c7a490 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,620 @@ +{ + "name": "tshy", + "version": "0.0.0-0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tshy", + "version": "0.0.0-0", + "license": "BlueOak-1.0.0", + "dependencies": { + "chalk": "^5.3.0", + "foreground-child": "^3.1.1", + "mkdirp": "^3.0.1", + "rimraf": "^5.0.1", + "sync-content": "^1.0.2", + "typescript": "5.2" + }, + "bin": { + "tshy": "dist/esm/index.js" + }, + "devDependencies": { + "@types/node": "^20.6.0", + "typedoc": "^0.25.1" + }, + "engines": { + "node": "16 >=16.17 || 18 >=18.16.0 || >=20.6.1" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/node": { + "version": "20.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz", + "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==", + "dev": true + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", + "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", + "dev": true + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob": { + "version": "10.3.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", + "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", + "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz", + "integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==", + "dependencies": { + "glob": "^10.2.5" + }, + "bin": { + "rimraf": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "0.14.4", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.4.tgz", + "integrity": "sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ==", + "dev": true, + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/sync-content": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/sync-content/-/sync-content-1.0.2.tgz", + "integrity": "sha512-znd3rYiiSxU3WteWyS9a6FXkTA/Wjk8WQsOyzHbineeL837dLn3DA4MRhsIX3qGcxDMH6+uuFV4axztssk7wEQ==", + "dependencies": { + "glob": "^10.2.6", + "mkdirp": "^3.0.1", + "path-scurry": "^1.9.2", + "rimraf": "^5.0.1" + }, + "bin": { + "sync-content": "dist/mjs/bin.mjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typedoc": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.1.tgz", + "integrity": "sha512-c2ye3YUtGIadxN2O6YwPEXgrZcvhlZ6HlhWZ8jQRNzwLPn2ylhdGqdR8HbyDRyALP8J6lmSANILCkkIdNPFxqA==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.3", + "shiki": "^0.14.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7486897 --- /dev/null +++ b/package.json @@ -0,0 +1,71 @@ +{ + "name": "tshy", + "version": "0.0.0-0", + "description": "TypeScript HYbridizer - Hybrid (CommonJS/ESM) TypeScript node package builder", + "author": "Isaac Z. Schlueter (https://izs.me)", + "license": "BlueOak-1.0.0", + "type": "module", + "bin": "./dist/esm/index.js", + "dependencies": { + "chalk": "^5.3.0", + "foreground-child": "^3.1.1", + "mkdirp": "^3.0.1", + "rimraf": "^5.0.1", + "sync-content": "^1.0.2", + "typescript": "5.2" + }, + "scripts": { + "postversion": "npm publish", + "prepublishOnly": "git push origin --follow-tags", + "prepare": "tsc -p tsconfig/esm.json && bash scripts/fixup.sh", + "pretest": "npm run prepare", + "presnap": "npm run prepare", + "format": "prettier --write . --loglevel warn --ignore-path ../../.prettierignore --cache", + "typedoc": "typedoc" + }, + "engines": { + "node": "16 >=16.17 || 18 >=18.16.0 || >=20.6.1" + }, + "repository": "https://github.com/isaacs/tshy", + "keywords": [ + "typescript", + "tsc", + "hybrid", + "esm", + "commonjs", + "build" + ], + "devDependencies": { + "@types/node": "^20.6.0", + "typedoc": "^0.25.1" + }, + "prettier": { + "semi": false, + "printWidth": 70, + "tabWidth": 2, + "useTabs": false, + "singleQuote": true, + "jsxSingleQuote": false, + "bracketSameLine": true, + "arrowParens": "avoid", + "endOfLine": "lf" + }, + "tshy": { + "dialects": [ + "esm" + ], + "exports": { + "./package.json": "./package.json", + ".": "./src/index.ts" + } + }, + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./dist/esm/index.d.ts", + "default": "./dist/esm/index.js" + } + } + } +} diff --git a/scripts/fixup.sh b/scripts/fixup.sh new file mode 100644 index 0000000..58590f3 --- /dev/null +++ b/scripts/fixup.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +chmod 0755 .tshy-build-tmp/esm/index.js +sync-content .tshy-build-tmp dist +rm -rf .tshy-build-tmp diff --git a/src/bins.ts b/src/bins.ts new file mode 100644 index 0000000..202c6d3 --- /dev/null +++ b/src/bins.ts @@ -0,0 +1,15 @@ +// chmod bins after build +import { chmodSync } from 'fs' +import { resolve } from 'path' +import pkg from './package.js' +export default () => { + const { bin } = pkg + if (!bin) return + if (typeof bin === 'string') { + chmodSync(resolve(bin), 0o755) + } else { + for (const v of Object.values(bin)) { + chmodSync(resolve(v), 0o755) + } + } +} diff --git a/src/build.ts b/src/build.ts new file mode 100644 index 0000000..e175a97 --- /dev/null +++ b/src/build.ts @@ -0,0 +1,64 @@ +import chalk from 'chalk' +import { spawnSync, SpawnSyncReturns } from 'node:child_process' +import { renameSync } from 'node:fs' +import { relative, resolve } from 'node:path/posix' +import { rimrafSync } from 'rimraf' +import { syncContentSync } from 'sync-content' +import bins from './bins.js' +import dialects from './dialects.js' +import { fail } from './fail.js' +import polyfills from './polyfills.js' +import setFolderDialect from './set-folder-dialect.js' +import './tsconfig.js' +import writePackage from './write-package.js' + +const buildFail = (res: SpawnSyncReturns) => { + setFolderDialect('src') + fail('build failed') + console.error(res) + process.exit(1) +} + +rimrafSync('.tshy-build-tmp') + +if (dialects.includes('esm')) { + setFolderDialect('src', 'esm') + const res = spawnSync('tsc -p .tshy/esm.json', { + shell: true, + stdio: 'inherit', + }) + setFolderDialect('src') + if (res.status || res.signal) buildFail(res) + setFolderDialect('.tshy-build-tmp/esm', 'esm') + console.error(chalk.cyan.bold('built esm')) +} + +if (dialects.includes('commonjs')) { + setFolderDialect('src', 'commonjs') + const res = spawnSync('tsc -p .tshy/commonjs.json', { + shell: true, + stdio: 'inherit', + }) + setFolderDialect('src') + if (res.status || res.signal) buildFail(res) + setFolderDialect('.tshy-build-tmp/commonjs', 'commonjs') + console.error(chalk.cyan.bold('built commonjs'), res) + // apply polyfills + for (const [f, t] of polyfills.entries()) { + const stemFrom = resolve( + '.tshy-build-tmp/commonjs', + relative(resolve('src'), resolve(f)) + ).replace(/\.cts$/, '') + const stemTo = resolve( + '.tshy-build-tmp/commonjs', + relative(resolve('src'), resolve(t)) + ).replace(/\.ts$/, '') + renameSync(`${stemFrom}.cjs`, `${stemTo}.js`) + renameSync(`${stemFrom}.d.cts`, `${stemTo}.d.ts`) + } +} + +syncContentSync('.tshy-build-tmp', 'dist') +rimrafSync('.tshy-build-tmp') +bins() +writePackage() diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..414c528 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,117 @@ +// get the config and package and stuff + +import { fail } from './fail.js' +import { Dialect, Package, TshyConfig, TshyExport } from './types.js' + +const validConfig = (e: any): e is TshyConfig => + !!e && + typeof e === 'object' && + (e.exports === undefined || validExports(e['exports'])) && + (e.dialects === undefined || validDialects(e['dialects'])) + +const isDialect = (d: any): d is Dialect => + d === 'commonjs' || d === 'esm' + +const validDialects = ( + d: any +): d is Exclude => + !!d && Array.isArray(d) && !d.some(d => !isDialect(d)) + +const validExports = ( + e: any +): e is Exclude => { + if (!e) return false + if (typeof e !== 'object') return false + for (const [sub, exp] of Object.entries(e)) { + if (sub !== '.' && !sub.startsWith('./')) { + fail( + `tshy.exports key must be "." or start with "./", got: ${sub}` + ) + process.exit(1) + } + if (typeof exp === 'string') { + e[sub] = addDot(exp) + continue + } + if (typeof exp !== 'object' || !exp || Array.isArray(exp)) { + fail( + `tshy.exports ${sub} value must be string or import/require object, ` + + `got: ${JSON.stringify(exp)}` + ) + process.exit(1) + } + const { import: i, require: r } = exp as Exclude< + TshyExport, + string + > + if (!i && !e) { + fail( + `tshy.exports ${sub} needs require or import, ` + + `got: ${JSON.stringify(exp)}` + ) + process.exit(1) + } + if ( + (i !== undefined && typeof i !== 'string') || + (r !== undefined && typeof r !== 'string') + ) { + fail( + `tshy.exports ${sub} import/require must be strings, ` + + `got: ${JSON.stringify(exp)}` + ) + process.exit(1) + } + if ( + (i !== undefined && join(i).startsWith('src/')) || + (r !== undefined && join(r).startsWith('src/')) + ) { + fail( + `tshy.exports ${sub} in src/ must be string paths, ` + + `got: ${JSON.stringify(exp)}` + ) + process.exit(1) + } + e[sub] = {} + if (e[sub].types) e[sub].types = addDot(e[sub].types) + if (e[sub].import) e[sub].types = addDot(e[sub].import) + if (e[sub].require) e[sub].types = addDot(e[sub].require) + } + if (e.dialects) { + if (!validDialects(e.dialects)) { + fail( + `tshy.dialects must be array containing 'esm' and/or 'commonjs', ` + + `got: ${JSON.stringify(e.dialects)}` + ) + } + } + return true +} + +const addDot = (s: string) => `./${join(s)}` + +const getConfig = ( + pkg: Package, + sources: Set +): TshyConfig => { + const tshy: TshyConfig = validConfig(pkg.tshy) ? pkg.tshy : {} + if (tshy.exports) return tshy + const e: Exclude = { + './package.json': './package.json', + } + for (const i of sources) { + if (/^\.\/src\/index\.[^\.]+$/.test(i)) { + e['.'] = i + break + } + } + pkg.tshy = tshy + tshy.exports = e + return tshy +} + +import { join } from 'path/posix' +import pkg from './package.js' +import sources from './sources.js' + +const config: TshyConfig = getConfig(pkg, sources) +export default config diff --git a/src/dialects.ts b/src/dialects.ts new file mode 100644 index 0000000..0b2beb4 --- /dev/null +++ b/src/dialects.ts @@ -0,0 +1,2 @@ +import config from './config.js' +export default config.dialects || ['esm', 'commonjs'] diff --git a/src/exports.ts b/src/exports.ts new file mode 100644 index 0000000..d566d64 --- /dev/null +++ b/src/exports.ts @@ -0,0 +1,105 @@ +import { relative, resolve } from 'node:path/posix' +import config from './config.js' +import dialects from './dialects.js' +import polyfills from './polyfills.js' +import { Export, TshyConfig, TshyExport } from './types.js' + +const getImpTarget = ( + s: string | TshyExport | undefined +): string | undefined => { + if (s === undefined) return undefined + if (typeof s === 'string') { + const imp = s.endsWith('.cts') ? undefined : s + return !imp || !imp.startsWith('./src/') + ? imp + : dialects.includes('esm') + ? `./dist/esm/${relative( + resolve('./src'), + resolve(imp) + ).replace(/\.(m?)tsx?$/, '.$1js')}` + : undefined + } + if (s && typeof s === 'object') { + return getImpTarget(s.import) + } +} + +const getReqTarget = ( + s: string | TshyExport | undefined, + polyfills: Map +): string | undefined => { + if (s === undefined) return undefined + if (typeof s === 'string') { + const req = s.endsWith('.mts') ? undefined : s + return !req || !req.startsWith('./src/') + ? req + : dialects.includes('commonjs') + ? `./dist/commonjs/${relative( + resolve('./src'), + resolve(polyfills.get(req) || req) + ).replace(/\.(m?)tsx?$/, '.$1js')}` + : undefined + } + if (s && typeof s === 'object') { + return getReqTarget(s.require, polyfills) + } +} + +const getExports = ( + c: TshyConfig, + polyfills: Map +): Record => { + if (!c.exports) { + fail('no exports on tshy config (is there code in ./src?)') + process.exit(1) + } + const e: Record = {} + for (const [sub, s] of Object.entries(c.exports)) { + const impTarget = getImpTarget(s) + const reqTarget = getReqTarget(s, polyfills) + + // only possible for exports outside of ./src + const types = typeof s !== 'string' && s.types + + if (typeof s !== 'string' || !s.startsWith('./src/')) { + if (impTarget === reqTarget) { + if (impTarget === undefined) continue + if (types) { + e[sub] = { + import: impTarget, + require: reqTarget, + types, + } + } else if (impTarget !== undefined) e[sub] = impTarget + continue + } + if (types) { + e[sub] = { + types, + import: impTarget, + require: reqTarget, + } + continue + } + } + + const exp: Export = (e[sub] = {}) + if (impTarget) { + exp.import = { + types: impTarget.replace(/\.(m?)js$/, '.d.$1ts'), + default: impTarget, + } + } + if (reqTarget) { + exp.require = { + types: reqTarget.replace(/\.(c?)js$/, '.d.$1ts'), + default: reqTarget, + } + } + } + return e +} + +import { fail } from './fail.js' +import pkg from './package.js' +export default pkg.exports = getExports(config, polyfills) diff --git a/src/fail.ts b/src/fail.ts new file mode 100644 index 0000000..174d350 --- /dev/null +++ b/src/fail.ts @@ -0,0 +1,5 @@ +import chalk from 'chalk' +export const fail = (message: string, er?: Error) => { + console.error(chalk.red.bold(message)) + if (er) console.error(er.message) +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..c6c4cf1 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +import chalk from 'chalk' + +import './exports.js' +import pkg from './package.js' + +const { exports: exp, tshy } = pkg + +console.error(chalk.yellow.bold('building')) +console.error(chalk.cyan.dim('tshy config'), tshy) +console.error(chalk.cyan.dim('exports'), exp) + +// have our config, time to build +await import('./build.js') + +console.log(chalk.bold.green('success!')) diff --git a/src/package.ts b/src/package.ts new file mode 100644 index 0000000..3156d5f --- /dev/null +++ b/src/package.ts @@ -0,0 +1,18 @@ +// get the package.json data for the cwd + +import { readFileSync } from 'fs' +import { fail } from './fail.js' +import { Package } from './types.js' + +const readPkg = (): Package => { + try { + return JSON.parse(readFileSync('package.json', 'utf8')) + } catch (er) { + fail('failed to read package.json', er as Error) + process.exit(1) + } +} + +const pkg = readPkg() +pkg.type = 'module' +export default pkg diff --git a/src/polyfills.ts b/src/polyfills.ts new file mode 100644 index 0000000..97d37b2 --- /dev/null +++ b/src/polyfills.ts @@ -0,0 +1,21 @@ +// the modules like -cjs.cts that override a module at .ts +import sources from './sources.js' +import chalk from 'chalk' + +const getPolyfills = (sources: Set): Map => + new Map( + [...sources] + .filter( + f => + f.endsWith('-cjs.cts') && + sources.has(f.replace(/-cjs\.cts$/, '.ts')) + ) + .map(f => [f, f.replace(/-cjs\.cts$/, '.ts')]) + ) + +const polyfills = getPolyfills(sources) +if (polyfills.size) { + console.error(chalk.cyan.dim('polyfills detected'), polyfills) +} + +export default getPolyfills(sources) diff --git a/src/set-folder-dialect.ts b/src/set-folder-dialect.ts new file mode 100644 index 0000000..74fa3dc --- /dev/null +++ b/src/set-folder-dialect.ts @@ -0,0 +1,16 @@ +import { writeFileSync } from 'fs' +import { rimrafSync } from 'rimraf' +import { Dialect } from './types.js' + +const writeDialectPJ = (f: string, mode?: Dialect) => + mode + ? writeFileSync( + f, + JSON.stringify({ + type: mode === 'commonjs' ? 'commonjs' : 'module', + }) + ) + : rimrafSync(f) + +export default (where: string, mode?: Dialect) => + writeDialectPJ(`${where}/package.json`, mode) diff --git a/src/sources.ts b/src/sources.ts new file mode 100644 index 0000000..d1d7dfa --- /dev/null +++ b/src/sources.ts @@ -0,0 +1,20 @@ +// get the list of sources in ./src + +import { readdirSync } from 'fs' +import { join } from 'path/posix' + +const getSources = (dir = 'src'): string[] => { + const sources: string[] = [] + const entries = readdirSync(dir, { withFileTypes: true }) + for (const e of entries) { + const j = `./${join(dir, e.name)}` + if (e.isFile()) sources.push(j) + else if (e.isDirectory()) { + sources.push(...getSources(j)) + } + } + return sources +} + +const sources = new Set(getSources()) +export default sources diff --git a/src/tsconfig.ts b/src/tsconfig.ts new file mode 100644 index 0000000..f2caed5 --- /dev/null +++ b/src/tsconfig.ts @@ -0,0 +1,65 @@ +import chalk from 'chalk' +import { existsSync, writeFileSync } from 'fs' +import { mkdirpSync } from 'mkdirp' + +// the commonjs build needs to exclude anything that will be polyfilled +import polyfills from './polyfills.js' + +const recommended: Record = { + compilerOptions: { + jsx: 'react', + declaration: true, + declarationMap: true, + inlineSources: true, + esModuleInterop: true, + forceConsistentCasingInFileNames: true, + moduleResolution: 'nodenext', + resolveJsonModule: true, + skipLibCheck: true, + sourceMap: true, + strict: true, + target: 'es2022', + module: 'nodenext', + }, +} + +const build: Record = { + extends: '../tsconfig.json', + compilerOptions: { + rootDir: '../src', + target: 'es2022', + module: 'nodenext', + moduleResolution: 'nodenext', + }, +} + +const commonjs: Record = { + extends: './build.json', + include: ['../src/**/*.ts', '../src/**/*.cts', '../src/**/*.tsx'], + exclude: [...polyfills.values()].map(f => `.${f}`), + compilerOptions: { + outDir: '../.tshy-build-tmp/commonjs', + }, +} + +const esm: Record = { + extends: './build.json', + include: ['../src/**/*.ts', '../src/**/*.cts', '../src/**/*.tsx'], + compilerOptions: { + outDir: '../.tshy-build-tmp/esm', + }, +} + +mkdirpSync('.tshy') +const writeConfig = (name: string, data: Record) => + writeFileSync( + `.tshy/${name}.json`, + JSON.stringify(data, null, 2) + '\n' + ) + +console.error(chalk.cyan.dim('writing tsconfig files...')) +if (!existsSync('tsconfig.json')) + writeConfig('../tsconfig', recommended) +writeConfig('build', build) +writeConfig('commonjs', commonjs) +writeConfig('esm', esm) diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..9e398e1 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,43 @@ +export type TshyConfig = { + exports?: Record + dialects?: Dialect[] +} + +export type Dialect = 'commonjs' | 'esm' + +export type TshyExport = + | string + | ({ types?: string; import?: string; require?: string } & ( + | { import: string } + | { require: string } + )) + +export type Package = { + name: string + version: string + type?: 'module' + bin?: string | Record + exports: Record + tshy?: TshyConfig + [k: string]: any +} + +// VERY limited subset of the datatypes "exports" can be +// but we're only writing our flavor, so it's fine. +export type Export = + | string + | { import?: string; require?: string; types?: string } + | { + import?: + | string + | { + types: string + default: string + } + require?: + | string + | { + types: string + default: string + } + } diff --git a/src/write-package.ts b/src/write-package.ts new file mode 100644 index 0000000..192e13b --- /dev/null +++ b/src/write-package.ts @@ -0,0 +1,6 @@ +import { writeFileSync } from 'fs' +import pkg from './package.js' + +export default () => { + writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n') +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..42776f8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "jsx": "react", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "nodenext", + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "target": "es2022", + "module": "nodenext" + } +} diff --git a/tsconfig/esm.json b/tsconfig/esm.json new file mode 100644 index 0000000..3b94cde --- /dev/null +++ b/tsconfig/esm.json @@ -0,0 +1,7 @@ +{ + "extends": "../tsconfig.json", + "include": ["../src/**/*.ts"], + "compilerOptions": { + "outDir": "../.tshy-build-tmp/esm" + } +} diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..d56e2a4 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,7 @@ +{ + "tsconfig": "./tsconfig/esm.json", + "entryPoints": ["./src/**/*.+(ts|tsx|mts|cts)"], + "navigationLinks": { + "isaacs projects": "https://isaacs.github.io/" + } +}