Types for sub modules
2022/03/24
When you build multiple entries in a single package, you exports them with exports
syntax. Like
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./foo": {
"types": "./dist/foo.d.ts",
"import": "./dist/foo.mjs",
"require": "./dist/foo.cjs"
},
}
}
Then tho you provide types
field for the sub modules, most of the users still got the error:
Cannot find module 'my-pkg/foo' or its corresponding type declarations.
Well that’s because the types
field in exports
will only be resolved when you add "moduleResolution": "NodeNext"
to the tsconfig.json
file. Which might cause more issue since not all the packages are up to date.
So when you trying to import my-pkg/foo
, TypeScript actually looking for the foo.d.ts
file under your package root instead of your dist
folder. One solution I been used for a long time is to create a redirection file that published to npm, like:
// foo.d.ts
export { default } from './dist/foo.d.ts'
export * from './dist/foo.d.ts'
Which solve the problem, but also making your root directory quite messy.
Until @tmkx shared me this solution:
{
"typesVersions": {
"*": {
"*": [
"./dist/index.d.ts",
"./dist/*"
]
}
}
}
Good day! Reference
range
in JavaScript
2021/09/13
Credit to GitHub Copilot.
I didn’t know you could provide a map function to Array.from
as a second argument until today.
Array.from({ length: 10 }, (_, i) => i)
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Clean npm cache
2021/09/08
My disk is full today :(
npm cache clean --force
yarn cache clean
pnpm store prune
Isomorphic __dirname
2021/08/30
In ESM, you might found your old friends __dirname
and __filename
are no longer available. When you search for solutions, you will find that you will need to parse import.meta.url
to get the equivalents. While most of the solutions only show you the way to get them in ESM only, If you like me, who write modules in TypeScript and transpile to both CJS and ESM at the same time using tools like tsup
. Here is the isomorphic solution:
import { dirname } from 'path'
import { fileURLToPath } from 'url'
const _dirname = typeof __dirname !== 'undefined'
? __dirname
: dirname(fileURLToPath(import.meta.url))
GitHub Co-authors
2021/08/24
You might found GitHub sometimes shows you a commit with multiple authors. This is commonly happening in squashed pull requests when multiple people are involved with the reviewing and made suggestions or changes. In that situation, GitHub will automatically inject the Co-authored-by:
to the commit message. This is a great way to give contributors credits while keeping the commit history clean.
Note that the format is like Co-authored-by: name <name@example.com>
, normally GitHub will fill that for you so you don’t need to worry about that, but if you want to add it manually, you have to get the email addresses of the contributors. But how do you know their emails?
Well, technically you can indeed find their email by multiple ways, but actually, you don’t need to. The easiest way is to copy their user id and append with @users.noreply.github.com
that provided by GitHub automatically, for example:
Co-authored-by: antfu <antfu@users.noreply.github.com>
Get Package Root
2021/07/14
When you want to get the real file path of a certain package, you could use require.resolve
to fetch their main entry path.
> require.resolve('vite')
'/Users/.../node_modules/vite/dist/node/index.js'
> require.resolve('windicss')
'/Users/.../node_modules/windicss/index.js'
However, when you want to get the root directory of the package, you will find the result of require.resolve
could vary based on different packages’ configurations.
A trick for this is to resolve the package.json
instead, as the package.json
is always located at the root of the package. Combining with path.dirname
, you could always get the package root.
> path.dirname(require.resolve('vite/package.json'))
'/Users/.../node_modules/vite'
Optimize Await
2021/07/01
async
/ await
in ES7 is truly a life-saver for the JavaScript world. It allows you to avoid callback hell in your code and make it more readable. However, a common pitfall is that when you await
a huge asynchronous task that takes very long time, it blocks the following code and could potentially make your app slow.
For example:
const app = await createServer()
const middlewareA = await resolveMiddlewareA()
const middlewareB = await resolveMiddlewareB()
app.use(middlewareA)
app.use(middlewareB)
We have used three await
in the example, while the three async function does not actually relying on each other, having them sequentially we are possibility wasted some time of the operations that could be parallelized (IO, Network, etc.)
So we can use Promise.all
to optimize the code:
const [app, middlewareA, middlewareB] = await Promise.all(
[
createServer(),
resolveMiddlewareA(),
resolveMiddlewareB(),
],
)
app.use(middlewareA)
app.use(middlewareB)
In another example, you might relying on the async result, but sometime not that urgent:
async function createPlugin() {
const toolkit = await initToolKit()
return {
onHookA() {
toolkit.invokeA()
},
onHookB() {
toolkit.invokeB()
},
}
}
const plugin = await createPlugin()
Even though you don’t need toolkit
immediately, you are still forced to use async function
because the initToolKit
is async. To avoid this, we could make the promise been resolved in the hooks instead
function createPlugin() {
const toolkitPromise = initToolKit()
return {
async onHookA() {
const toolkit = await toolkitPromise
toolkit.invokeA()
},
async onHookB() {
const toolkit = await toolkitPromise
toolkit.invokeB()
},
}
}
// now it's sync!
const plugin = createPlugin()
Since a Promise could only be resolved once, using multiple await
for a single Promise instance is totally fine - it will return the resolved result immediate if the Promise is allready settled.
To be more generalized, we could have an utility function like:
export function createSingletonPromise<T>(fn: () => Promise<T>) {
let _promise: Promise<T> | undefined
return () => {
if (!_promise)
_promise = fn()
return _promise
}
}
This function is also available in my utilities collection @antfu/utils
Typed provide
and inject
in Vue
2021/03/05
I didn’t know that you can type provide()
and inject()
elegantly until I watched Thorsten Lünborg’s talk on Vue Amsterdam.
The basic idea here is the Vue offers a type utility InjectionKey
will you can type a Symbol to hold the type of your injection values. And when you use provide()
and inject()
with that symbol, it can infer the type of provider and return value automatically.
For example:
// context.ts
import type { InjectionKey } from 'vue'
export interface UserInfo {
id: number
name: string
}
export const InjectKeyUser: InjectionKey<UserInfo> = Symbol()
// parent.vue
import { provide } from 'vue'
import { InjectKeyUser } from './context'
export default {
setup() {
provide(InjectKeyUser, {
id: '117', // type error: should be number
name: 'Anthony',
})
},
}
// child.vue
import { inject } from 'vue'
import { InjectKeyUser } from './context'
export default {
setup() {
const user = inject(InjectKeyUser) // UserInfo | undefined
if (user)
console.log(user.name) // Anthony
},
}
See the docs for more details.
Color Scheme for VS Code Extensions
2021/03/01
There is currently no API to access colors of current theme in VS Code Extensions, nor the meta information of them. It frustrated me for a long while, until today I came up with a dirty but working solution.
Since most of the themes follow the conversions of having Light
or Dark
in their names. Then we can have:
import { workspace } from 'vscode'
export function isDarkTheme() {
const theme = workspace.getConfiguration()
.get('workbench.colorTheme', '')
// must be dark
if (theme.match(/dark|black/i) != null)
return true
// must be light
if (theme.match(/light/i) != null)
return false
// IDK, maybe dark
return true
}
Simple, but surprisingly, it works really well. This is used for my Browse Lite extension to inject the preferred color schema matching with VS Code’s theme. And also Iconify IntelliSense for VS Code to update icons color with the theme.
Type Your Config
2021/02/29
Configurations can be quite complex, and sometimes you may want to utilize the great type checking that TypeScript provided. Change your xxx.config.js
to xxx.config.ts
is not an ideal solutions as you will need to have a Node.js register involved to transpile it into JavaScript and some tools might not support doing that way. Fortunately, TypeScript also support type check in plain JavaScript file with JSDoc. Here is an example of Webpack config with type checks:
// webpack.config.js
// @ts-check
/**
* @type {import('webpack').Configuration}
*/
const config = {
/* ... */
}
module.exports = config
Prefect. Everything should work and you can already call it a day.
I have never thought about we can do better, until I saw Vite’s approach. In Vite, you can simply have:
// vite.config.js
import { defineConfig } from 'vite'
export default defineConfig({
/* ... */
})
No JSDocs, no need to declare a variable first then export it. And since TypeScript will infer the types even you are using plain JavaScript, it works great with both.
How? The defineConfig
is literally a pass-through, but brings with types:
import type { UserConfig } from 'vite'
export function defineConfig(options: UserConfig) {
return options
}
defineConfig
exists in the runtime, so it works for JavaScript even if the types get truncated. This is really just some small details of DX, but I would wish more tools could adapt this approach and make the type checking more approachable and simpler.
Match Quotes in Pairs
2021/02/28
In JavaScript, single quotes('') and double quotes("") are interchangeable. With ES6, we now even have backticks(``) for template literals. When you want to write a quick script to find all the strings without introducing a heavy parser, you may think about using RegExp. For example, you can have:
/['"`](.*?)['"`]/gm
It works for most of the case, but not for mixed quotes:
'const a = "Hi, I\'m Anthony"'.match(/['"`](.*)['"`]/m)[1] // "Hi, I"
You have to make sure the starting quote and ending quote are the same type. Initially I thought it was impossible to do it in RegExp, or we have to do like this:
/'(.*?)'|"(.*?)"|`(.*?)`/gm
That’s definitely a bad idea as it makes you duplicated your notations. Until I found this solution:
/(['"`])(.*?)\1/gm
\1
is a Backreferences to your first group, similarly you can have \2
for the second group 2 and \3
for the third, you got the idea. This is exactly what I need! Take it a bit further, to exclude the backslash escaping, now we can have a much reliable RegExp for extracting quoted texts from any code.
/(["'`])((?:\\\1|(?:(?!\1)|\n|\r).)*?)\1/mg
You can find it running in action on my vite-plugin-windicss
.
Match Chinese Characters
2021/02/25
When you need to detect if a string contains Chinese characters, you would commonly think about doing it will RegExp, or grab a ready-to-use package on npm.
If you Google it, you are likely end up with this solution:
/[\u4E00-\u9FCC\u3400-\u4DB5\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|[\uD86A-\uD86C][\uDC00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D]/
It works, but a bit dirty. Fortunately, I found a much simpler solution today:
/\p{Script=Han}/u
!!'你好'.match(/\p{Script=Han}/u) // true
It’s called Unicode property escapes and already available in Chrome 64, Firefox 79, Safari 11.1 and Node.js 10.
Netlify Redirects (Domains)
2021/02/20
On Netlify, you can setup multiple domains for a site. When you add a custom domain, the xxx.netlify.app
is still accessible. Which would potentially cause some confusion to users. In that way, you can setup the redirection in your netlify.toml
file, for example:
[[redirects]]
from = "https://vueuse.netlify.app/*"
to = "https://vueuse.org/:splat"
status = 301
force = true
*
and:splat
mean it will redirect all the sub routes as-is to the new domain.force = true
specifying it will always redirect even if the request page exists.
Netlify Redirects (Site names)
2021/02/20
Unlike domain redirection, sometimes you would need to rename the Netlify subdomain name (a.k.a site name), for example xxx.netlify.app
to yyy.netlify.app
. After you do the rename, people visiting xxx.netlify.app
will receive a 404. And since you no longer have controls over xxx.netlify.app
, you can’t just setup a redirect in your new site.
A solution here is to create a new site with your original name xxx
and upload the redirection rules. In this case, we can have netlify.toml
like this:
[[redirects]]
from = "*"
to = "https://yyy.netlify.app/:splat"
status = 301
force = true
Note you don’t have to link a repo to that, Netlify offers a great feature that let you drag and drop for static files and serve as a site. So you can just save netlify.toml
and upload it, rename the site to your original name. The redirection is done!