-
Notifications
You must be signed in to change notification settings - Fork 6.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(learn): add article for publishing a typescript package #7279
base: main
Are you sure you want to change the base?
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
31678d2
to
2d5d359
Compare
Jacob you should:
|
fail-fast: false # prevent a failure in one version cancelling other runs | ||
|
||
steps: | ||
- uses: actions/checkout@v4 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these version refs will likely get outdated pretty fast; what’s the plan for keeping them updated?
This comment was marked as outdated.
This comment was marked as outdated.
Co-authored-by: Augustin Mauroy <augustin.mauroy@outlook.fr> Signed-off-by: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for writing this up! I have some tsconfig feedback. Also, is it worth mentioning anything about package.json fields (i.e. that they should reference .js
files, not .ts
files)?
"lib": ["ESNext"], | ||
"module": "NodeNext", | ||
"moduleResolution": "NodeNext", | ||
"outDir": "./", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the default, but we generally recommend setting outDir
to something else. ("rootDir": "src", "outDir": "dist"
is common.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I was also about to comment that when I noticed the flat layout above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the reason for that recommendation?
"outDir": "dist"
will cause that folder to get created and published though, whereas I want it to be in just the root to avoid unnecessary drilling.
So I think I want neither "rootDir": "src"
nor "outDir": "dist"
, because that will double what I want to avoid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, it at least implies that any large project is just going to dump a load of files at the root, which are going to be very annoying to gitignore
/npmignore
/eslitignore
/prettierignore
/etc, and visually ignore in the repo. If you output to dist
, they're all in one place, easy to ignore, and notably, less likely to be accidentally loaded by TS or something.
I haven't personally seen a project which used a src
and then dumped files at the root, only src
-> dist
, or, no outDir
and allow the emitted JS to live next to the TS.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It also makes it easier to throw away all the files for a clean build. With package.json exports
, it feels like there really isn't much reason to publish at the root anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you say what those issues are? The only thing I can think of is compat with old node releases that do not support exports, or trying to support people who want to deep import when the package author does not want that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it preserves structure, but that feels beside the point. But by and large, the recommendation we've had (and really the de facto norm at this point) is to use a dedicated output directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's very atypical for anyone to run
tsc --noEmit
and then also runtsc
and publish its outputs.
Maaybe there's a misunderstanding: these would not be run one right after the other:
- On a PR,
tsc --noEmit
would run. - On a release,
tsc
would run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand that, I'm just saying that the two may behave differently as there are errors that can only happen when tsc
is emitting, and you would never see them until tsc
is run without --noEmit
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a PR,
tsc --noEmit
would run.
Let me just say that a release-blocker because of .d.ts
emit problems is better caught as early as possible, and the delta in CI time is not worth it.
*.ts | ||
!*.d.ts | ||
*.fixture.* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If rootDir
is src
, src
can be ignored and then everything else will work.
These patterns will not correctly ignore any cts
/mts
files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These patterns will not correctly ignore any
cts
/mts
files.
Sorry, I don't understand why those are special cases. Could you please explain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intent of this npmignore appears to be to prevent inclusion of source files; but one can handwrite foo.mts
, which emits as foo.mjs
and foo.d.mts
, and so these globs will not handle them.
It's moot if you just ignore src
, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OH! You're talking about line 211. The code samples aren't using those extensions, so I didn't account for them to keep things simple and explicit.
Buuut that is a good idea. If we go this route btw, I think I should explain why this is a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OH! You're talking about line 211
Yeah, it's a multi-line code review comment 😅
|
||
## What to do with your types | ||
|
||
### Treat types like a test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a note: types do not sobstitute unit testing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just avoid mentioning "treat types like tests" in the first place? I don't really see how it weaves into the blog post (even in this section)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, we should treat types as a part of code quality checking. Like lintting and formatting
|
||
## What to do with your types | ||
|
||
### Treat types like a test |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add a note: types do not sobstitute unit testing
A published package will look something like: | ||
|
||
```text displayName="Published example TypeScript package (directory overview)" | ||
example-ts-pkg/ | ||
├ LICENSE | ||
├ main.d.ts | ||
├ main.d.ts.map | ||
├ main.js | ||
├ package.json | ||
├ README.md | ||
├ some-util.d.ts | ||
├ some-util.d.ts.map | ||
└ some-util.js | ||
``` | ||
|
||
That would be derived from a repository looking something like: | ||
|
||
```text displayName="Source of the example TypeScript package (directory overview)" | ||
example-ts-pkg/ | ||
├ .github/ | ||
├ workflows/ | ||
├ ci.yml | ||
└ publish.yml | ||
└ dependabot.yml | ||
├ src/ | ||
├ foo.fixture.js | ||
├ main.ts | ||
├ main.test.ts | ||
├ some-util.ts | ||
└ some-util.test.ts | ||
├ LICENSE | ||
├ package.json | ||
└ README.md | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these info need to be reverse/inverted because it's more logical to have "what you write" the "what you get (published)"
|
||
- Node runs TypeScript code via a process called "[type stripping](https://nodejs.org/api/typescript.html#type-stripping)", wherein node (via [Amaro](https://github.com/nodejs/amaro)) removes TypeScript-specific syntax, leaving behind vanilla JavaScript (which node already understands). This behaviour is enabled by default as of node version 23.6.0. | ||
|
||
- Node does **not** strip types in `node_modules` because it can cause significant performance issues for the official TypeScript compiler (`tsc`) and parts of VS Code, so the TypeScript maintainers would like to discourage people publishing raw TypeScript, at least for now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Node does **not** strip types in `node_modules` because it can cause significant performance issues for the official TypeScript compiler (`tsc`) and parts of VS Code, so the TypeScript maintainers would like to discourage people publishing raw TypeScript, at least for now. | |
- Node does **not** strip types in `node_modules` because it can cause significant performance issues for the official TypeScript compiler (`tsc`) and parts of VS Code, so the TypeScript maintainers would like to discourage people publishing raw TypeScript, at least for now. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The indentation wasn't an accident/typo, if that's what you're thinking. This could be root-level I suppose, but I was thinking it was an addendum to the item above it. I'm splitting hairs though don't feel strongly. If people thing it's better to keep root-level, sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I understood 😉 That is what I originally had, and it renders wonky on nodejs.org.
I think you meant to respond to this thread though? #7279 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I misread and responded to the wrong comment. 😅
example-ts-pkg/ | ||
├ .github/ | ||
├ workflows/ | ||
├ ci.yml |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It it intentional that inner connecting lines are omitted?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes: I had them, but on nodejs.org they got rendered weird and didn't align, so I removed them 😞
|
||
TypeScript has warned that the above code will not behave as intended, just like a unit test warns that code does not behave as intended. | ||
|
||
Your IDE (ex VS Code) likely has built-in support for TypeScript, displaying errors as you work. If not, and/or you missed those, CI will have your back. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your IDE (ex VS Code) likely has built-in support for TypeScript, displaying errors as you work. If not, and/or you missed those, CI will have your back. | |
Your editor (e.g. VS Code) likely has built-in support for TypeScript, displaying errors as you work. If not, and/or you missed those, CI will have your back. |
|
||
[`npm publish`](https://docs.npmjs.com/cli/v11/commands/npm-publish) grabs everything applicable and available at the moment the command is run; so generating type declarations immediately before means those are available and will get picked up. | ||
|
||
By default, `npm publish` grabs (almost) everything (see [Files included in package](https://docs.npmjs.com/cli/v11/commands/npm-publish#files-included-in-package)). In order to keep your published package minimal (see the "Heaviest Objects in the Universe" meme about `node_modules`), you want to exclude certain files (like tests and test fixtures) from from packaging. Add these to the opt-out list specified in [`.npmignore`](https://docs.npmjs.com/cli/v11/using-npm/developers#keeping-files-out-of-your-package); ensure the `!*.d.ts` exception is listed, or the generated type declartions will not be published! Alternatively, you can use [package.json "files"](https://docs.npmjs.com/cli/v11/configuring-npm/package-json#files) to create an opt-in list. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By default,
npm publish
grabs (almost) everything (see Files included in package).
Similar to what @andrewbranch and @jakebailey said above, if you specify an --outDir
, then you can use the package.json
"files"
array to avoid other hazards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or .npmignore
, which avoids hazards endemic to files
:-)
}, | ||
// These may be different for your repo: | ||
"include": ["./src"], | ||
"exclude": ["**/*/*.test.*", "**/*.fixture.*"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What exactly is .fixture.
supposed to imply? I haven't seen this convention.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can see it in the previous samples. It's like syntax-error.fixture.mjs
. Sometimes fixtures are all within a fixtures
directory, but if you have only 1 or 2 fixtures and won't have more, a directory might be bloating.
Thank you! TIL several things 😁
Oh! Yes! |
"name": "example-ts-pkg", | ||
"scripts": { | ||
"test": "node --test './src/**/*.test.ts'", | ||
"types:check": "tsc --noEmit" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noted this in the other thread, but I would be cautious about this as a default; I really only see people setting noEmit when they're doing a quick check, or are using a bundler or something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the type "test" command. Why would you want to emit a compilation?
Maybe the names could be better? When I have unit and end-to-end tests with different setups, I split those into different commands like:
test:unit
test:e2e
So in that scenario, it could make sense to name types:check
→ test:types
.
But in the sample, there's no differentiation between units and e2e, so then what do I call what is currently test
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, I guess it's fine, I am just wary of cases where tsc
and tsc --noEmit
output different errors because the former is doing more. Maybe you'd hit it on prepack
and that's okay, but it's a little unfortunate to only hit an error when you go to release...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is that likely? I've been doing this for years and never encountered that—am I just very lucky?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how to gauge "likely", probably unlikely, but they're cases like "tsc failed to write the files", along with potentially some declaration transform errors. (The latter shouldn't actually end up mattering by my reading of the code, though.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The specific issues that come up that I can think of are when a declaration file can't reliably be generated because doing so might require referencing entities that are private or non-exported. Trying to figure out why this error is happening can be pretty frustrating, especially if you've been relying a specific pattern over time. Having a divergence between publish/CI probably just makes this even more confusing since most people outside of the person who set up the build won't be aware of any differences.
}, | ||
// These may be different for your repo: | ||
"include": ["./src"], | ||
"exclude": ["**/*/*.test.*", "**/*.fixture.*"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would you exclude your tests from the tsconfig? Shouldn't they be typechecked?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They actually might need a separate tsconfig.json
if you don't want them in outDir
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, though I personally opt to put my tests in a specific folder like __tests__
just so I can exclude them and their tests easily, but I know some people make a second tsconfig and then build mode and so on (it's just too many steps for me to feel good about it).
name: Publish to NPM | ||
on: | ||
push: | ||
tags: | ||
- '**@*' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
name: Publish to NPM | |
on: | |
push: | |
tags: | |
- '**@*' | |
name: Publish to NPM | |
on: | |
release: | |
types: [published] |
We can propose to just use release not tag. In some case tag is useful for example when you have monorepo
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
? Package releases should always have a git tag, no exceptions - single-package repos too.
Description
Document the recommended way to publish a typescript package
Validation
Related Issues
nodejs/typescript#19
Depends on #7229
Check List
npm run format
to ensure the code follows the style guide.npm run test
to check if all tests are passing.npx turbo build
to check if the website builds without errors.