🚀 A project starter for module publisher
Find a file
2023-05-29 17:05:49 +02:00
.github Run test with only node 16 and 17 2022-10-27 23:19:06 +02:00
scripts Fix formating 2023-05-29 17:05:49 +02:00
src Add a src/tools dir to show exports 2023-05-21 10:14:54 +02:00
test Use vitest by default 2023-05-21 13:57:34 +02:00
.eslintignore Add linking script 2023-02-25 14:29:46 +01:00
.eslintrc.js Update .eslintrc.js 2021-05-21 08:40:36 +02:00
.gitattributes update 2021-08-01 04:02:57 +02:00
.gitignore Add linking script 2023-02-25 14:29:46 +01:00
.prettierignore Add linking script 2023-02-25 14:29:46 +01:00
.prettierrc.json Add linking script 2023-02-25 14:29:46 +01:00
LICENSE Add LICENSE files 2020-05-17 00:39:20 +02:00
LICENSE.template Add LICENSE files 2020-05-17 00:39:20 +02:00
package.json Use vitest by default 2023-05-21 13:57:34 +02:00
README.md Explain how to release a real ESM dist 2023-05-21 10:52:07 +02:00
README.template.md Explicitely support React out of the box 2023-05-21 08:04:44 +02:00
tsconfig.json Fix formating 2023-05-29 17:05:49 +02:00
vitest.config.ts Use vitest by default 2023-05-21 13:57:34 +02:00
yarn.lock Use vitest by default 2023-05-21 13:57:34 +02:00

🚀 A project starter 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.

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

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

And that's all there is to it!

🗣️ 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

Key features:

  • Unlike traditional CLI tools, ts-ci utilizes automation within Github Actions. Simply update your package.json version number and push. Your new version is automatically published on NPM.
  • It offers the convenience of publishing prereleases. All you need to do is update your package version to a prerelease format like 1.2.3-rc.3.
  • It fosters enhanced quality control as it runs tests against the submitter's fork whenever a PR is opened.
  • ts-ci doesn't bundle your library into a single file. Instead, users can cherry-pick imports from your library, enabling tree shaking. For instance: import { aSpecificFunction } from "your-module/aSpecificFile".

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

Examples of project using this template

How to use

  • Click on image
  • The repo name you will choose will be used as a module name for NPM.
  • Go to the repository Settings tab, then Secrets you will need to add a new secret: NPM_TOKEN, you NPM authorization token.
  • To trigger publishing edit the package.json version field ( 0.0.0-> 0.0.1 for example) then push changes... that's all !
  • Publish pre-release by setting your version number to X.Y.Z-rc.U (example: 1.0.0-rc.32). On NPM the version will be tagged next.
  • The CI runs on main and on the branches that have a PR open on main (You can publish pre-release from branches this way).

Features

  • ✍️ Assists in completing the package.json details.
  • Runs your test across various OSes and Node version combinations. Reference. Note: This might be overkill for most use-cases. Feel free to modify the matrix as per your needs.
  • 📦 Supports publishing on NPM along with creating corresponding GitHub releases.
  • 🧪 Enables testing of your local module copy in your application before publishing it on NPM.
  • 🌗 Offers flexibility to use different repository images for dark and light modes. For instance, check out i18nifty: Light and Dark. For implementation details, see here. TS-CI also provides an additional action that removes the dark mode specific image from your README.md before publishing on NPM, as NPM does not yet support the #gh-dark-mode-only syntax.
  • 🩳 By default, TS-CI incorporates a step in the workflow that relocates your distribution files to the root directory before releasing, allowing your users to import specific files from your module as import {...} from "my_module/theFile" rather than "my_module/dist/theFile". If you dislike this behavior or if you only have an index.ts file and do not intend for users to selectively import from your module, you may remove this action.
  • ⚙️ ESlint and Prettier are automatically triggered against files staged for commit. Despite what t3dotgg says, it's the correct way of doing it, that being said, this feature is optional and can be disabled if desired.

Release in CJS, ESM or both

CJS only (default)

By default your module release in CommonJS (CJS) with ESM module interop.

You want to avoid this strategy if:

  • Your module has peer dependencies that provides both an ESM and CJS distribution. (Example @mui/material, @emotion/react).
  • You make use of async imports (import(...).then(...))).
  • You want your module to be usable in node type: module mode AND you have some export default (if you don't have export default it will work just fine).

ESM only

If you want to only release as ESM just set "module": "ES6" in your tsconfig.json (and remove esModuleInterop). You can remove the listing of your export in the package.json it's not of any use.

You want to avoid this strategy if:

  • You want your module to be usable with node. The ESM distribution produced by TypeScript is an ESM distribution that node in type: module can process (files need to have .mjs extension, exports need to be listed).
    As a result your module won't be usable at all on node except through Next.js for example that will be able to make it work.
    This means for example that you'd have to tell your users to configure their JEST so that it transpiles your module using "transformIgnorePatterns": [ "node_modules/(?!@codegouvfr/react-dsfr)" ].
    If you publish scripts (your package.json has a bin property) you'll need to transpile your script separately in CJS.

ESM for bundlers (browser) + CJS for node.

  • Have a tsconfig.json that targets CSM (as by default): example
  • Perform two build, one for CJS, one for ESM. example
  • Explicitly list your exports in your package.json, "module" the condition for bundlers "default" is what will be picked up by node. example.

You want to avoid this strategy if:

  • You use export default and you want to support node in type: module mode.
  • You have lazy import (import(...).then(...)) and you want them to be lazy not only on the browser but on node too.

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 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.js.

Checkout the full example with tsafe (use the specifically this link, the current version doesn't release an ESM distribution anymore, it doesn't need to).

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.

How to debug the action

You can increase the verbosity by creating a new secret ACTIONS_STEP_DEBUG and setting it to true.

image

Disable linting and formatting

Remove this, this and this from your package.json
Remove this and this 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)

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

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

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:

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));
}