garronej / ts-ci

🚀 A project starter for module publisher
MIT License
351 stars 19 forks source link
npm npm-module npm-package prettier typescript

🚀 A starter project for module publisher! 🚀

Have you written some functions or React component that you're proud of? Do you want to share it as a standalone module on NPM, but find yourself unsure about the publishing process or how to manage the lifecycle of an open-source library?

Look no further - ts-ci is here to jumpstart your journey towards becoming a proficient NPM module author.

Main selling points:

https://user-images.githubusercontent.com/6702424/197344513-065246b9-8823-4894-a9a7-6c539da10655.mp4

Examples of project using this template

How to use

🗣️ Since a recent GitHub update you need to manually allow GitHub Action to push on your repo.
Fo this reason the initial setup will fail.
You need to enabled permission and re-run failed job: see video

Features

Release in CJS, ESM or both (but don't bundle!)

Contrary to what other guides or project starters may suggest, you don't need to bundle your library (or in other words you don't need to use Vite/Rollup), nor do you need to fragment your modules into smaller, independently published units on NPM under the packages/ directory for your module to be tree-shakable (e.g., @your-module/submodule1, @your-module/submodule2).

When you bundle your library, you incorporate all your dependencies into the .js code you distribute. This could potentially lead to duplication of dependencies.

For instance, if your library depends on the classnames package, the entirety of classnames source code will be included in your bundle. Subsequently, if a user of your library is also directly using classnames, there will be two copies of classnames in their final application bundle.

Another disadvantage of bundling is the lack of selective importing. For example, if a user wishes to import only a specific component or functionality from your module, they would be unable to do so. Instead, they are required to import the entire library.

Publishing independent units on NPM under the packages/ directory (e.g., @your-module/submodule1, @your-module/submodule2) is a common practice, but it's not necessarily a beneficial one. The first reason against this approach is that once you comprehend the fact that bundling isn't necessary, persisting with this process becomes somewhat pointless. The second reason concerns your users, who are then burdened with the responsibility of individually installing and synchronizing the various components' versions. This could cause potential inconsistencies and compatibility issues, making the user experience less efficient and potentially more troublesome.

The reality is much simpler. The responsibility of bundling lies with the final application; your role involves merely publishing types declaration (.d.ts) and the correct flavor of JavaScript files, which are the output of tsc.

That's all there is to it!

CJS only (default)

By default your module release in CommonJS (CJS).

You want to avoid this strategy if:

ESM only

If you want to only release as ESM just set "module": "ES2020" in your tsconfig.json, you also probably want to set "target": "ES2017". You can remove the listing of your export in the package.json it's not of any use.
This option has the advantage, if you are publishing a React library, to enable you to import assets file (.svg, .css) like for example here (Don't forget to copy your the assets from your src/ to your dist/ though, TypeScript don't do it for you).

You want to avoid this strategy if:

ESM for bundlers (browser) + CJS for node.

You want to avoid this strategy if:

Deno

Regardless of the scenario you opt for you can always release for Deno using Denoify.

CJS + A real ESM distribution, fully compliant with the standard

Pursuing a fully compliant CJS + ESM distribution comes with caveats. It only works well if all your dependencies are adherent to the standard, a condition that most modules fail to meet.

This method introduces the risk of your package being simultaneously loaded in both CJS and ESM in a single node application. It also poses a similar risk to your dependencies.

Thus, proceed with this option only if it's necessary for your lazy imports to actually be lazy when your code runs on Node.

I have questions

If you find your self thinking:

"I don't know man, ESM, CJS, I have no idea, I just want my stuff to work!" "None of the option above covers all my requirement?" "Why can't I have a solution that work in every case?"
"Why can't I publish an actual standard compliant ESM distribution?"

Just start a discussion or hit my Twitter DM I'll be happy to provide further guidance.

FAQ

Click to expand ## Can I use `npm` (or something else) instead of `yarn` Yes, just remove the `yarn.lock` file and edit `.github/workflows/ci.yaml`, replace all `yarn ***` by `npm run ****`. Note however that the the script (`scripts/link-in-app.ts`) that enable you to test in an external app will no longer work. ## What will be included in the npm bundle? All filles listed in [the files property of your package JSON](https://github.com/garronej/ts_ci/blob/974054f2b83f8170317f2b2fa60b5f78e9336c0b/package.json#L35-L41). ## How to debug the action You can increase the verbosity by creating a new secret `ACTIONS_STEP_DEBUG` and setting it to true. ![image](https://user-images.githubusercontent.com/6702424/144307837-f401c595-4695-45e3-8459-b1c1ca7fabb9.png) ## Disable linting and formatting Remove [this](https://github.com/garronej/ts_ci/blob/974054f2b83f8170317f2b2fa60b5f78e9336c0b/package.json#L15-L18), [this](https://github.com/garronej/ts_ci/blob/974054f2b83f8170317f2b2fa60b5f78e9336c0b/package.json#L20-L32) and [this](https://github.com/garronej/ts_ci/blob/974054f2b83f8170317f2b2fa60b5f78e9336c0b/package.json#L47-L53) from your `package.json` Remove [this](https://github.com/garronej/ts_ci/blob/974054f2b83f8170317f2b2fa60b5f78e9336c0b/.github/workflows/ci.yaml#L12-L26) and [this](https://github.com/garronej/ts_ci/blob/974054f2b83f8170317f2b2fa60b5f78e9336c0b/.github/workflows/ci.yaml#L29) from `github/workflows/ci.yaml` Remove `.eslintignore`, `.eslintrc.js`, `.prettierignore` and `.prettierrc.json`. ## Accessing files outside the ``dist/`` directory (when [this line is present in your repo](https://github.com/garronej/ts-ci/blob/eabbcfa5b22777c6b051206d8f4e2c8a8624c853/.github/workflows/ci.yaml#L100)) The drawback of having short import path is that the dir structure is not exactly the same in production ( in the npm bundle ) and in development. The files and directories in ``dist/`` will be moved to the root of the project. As a result this won't work in production: ``src/index.ts`` ```typescript import * as fs from "fs"; import * as path from "path"; const str = fs.readFileSync( path.join(__dirname,"..", "package.json") ).toString("utf8"); ``` Because ``/dist/index.js`` will be moved to ``/index.js`` You'll have to do: ``src/index.ts`` ```typescript import * as fs from "fs"; import * as path from "path"; import { getProjectRoot } from "./tools/getProjectRoot"; const str = fs.readFileSync( path.join(getProjectRoot(),"package.json") ).toString("utf8"); ``` With `getProjectRoot.ts` being: ```typescript import * as fs from "fs"; import * as path from "path"; function getProjectRootRec(dirPath: string): string { if (fs.existsSync(path.join(dirPath, "package.json"))) { return dirPath; } return getProjectRootRec(path.join(dirPath, "..")); } let result: string | undefined = undefined; export function getProjectRoot(): string { if (result !== undefined) { return result; } return (result = getProjectRootRec(__dirname)); } ```