Skip to content

Commit

Permalink
tshy: a TypeScript HYbrid builder
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacs committed Sep 11, 2023
0 parents commit 31ec84b
Show file tree
Hide file tree
Showing 25 changed files with 1,631 additions and 0 deletions.
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
50 changes: 50 additions & 0 deletions .github/workflows/typedoc.yml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.tshy*
/node_modules
/dist
63 changes: 63 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -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
<https://blueoakcouncil.org/license/1.0.0>.

## 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.***
235 changes: 235 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
`<name>-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 `<name>.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/<whatever>.ts` and the CommonJS polyfill living in
`src/<whatever>-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.
Loading

0 comments on commit 31ec84b

Please sign in to comment.