I'd like to centralize my generic most-used (typescript) functions in a Util package which I can reuse across my projects. Turned out more difficult than expected. This package is not going to be published, so I'm really only interested in ESM.
I was able to do this as a plain js-package, but now that I'm converting it to TS, I'm running into problems.
My question is, how to import from an external package? I use various Lodash functions. But Rollup complains that they don't exist, and/or have to be exported as well.
I've included the first function that I was putting into this lib, I'm very new to TS, so don't mind that too much. ;-)
[!] RollupError: "now" is not exported by "../../node_modules/.pnpm/lodash@4.17.21/node_modules/lodash/lodash.js", imported by "out-tsc/src/createUid.js".
https://rollupjs.org/troubleshooting/#error-name-is-not-exported-by-module
out-tsc/src/createUid.js (1:9)
1: import { now, random, padStart } from "lodash";
^
This my latests setup, going through many many variations:
Config
package.json
{
"name": "@vexna/util",
"version": "1.0.0",
"description": "Generic utilities, uses lodash",
"private": true,
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"sideEffects": false,
"scripts": {
"build": "rimraf dist && tsc && rollup -c rollup.config.js",
"test": "node test/spec",
"pretest": "npm run build"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@open-wc/building-rollup": "^2.2.1",
"@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-node-resolve": "^15.0.1",
"@types/lodash": "^4.14.191",
"deepmerge": "^4.3.0",
"lodash": "^4.17.21",
"rimraf": "^4.1.2",
"rollup": "^3.12.1",
"typescript": "^4.9.5"
},
"peerDependencies": {
"lodash": "^4.17.21"
},
"files": [
"dist"
]
}
tsconfig.json
{
"compilerOptions": {
"target": "es2018",
"module": "esnext",
"moduleResolution": "node",
"noEmitOnError": true,
"lib": ["es2017"],
"strict": true,
"esModuleInterop": false,
"outDir": "out-tsc",
"rootDir": "./",
"skipLibCheck": true,
"declaration": true,
"allowSyntheticDefaultImports": true
},
"include": ["./src/**/*.ts"]
}
rollup.config.js
import merge from 'deepmerge';
import { createBasicConfig } from '@open-wc/building-rollup';
const baseConfig = createBasicConfig();
export default merge(baseConfig, {
input: ['./out-tsc/src/index.js'],
output: {
format: "esm",
exports: "named",
dir: 'dist',
},
external: ['loadash'],
});
.babelrc
{
"presets": [["@babel/env", { "modules": false }]]
}
Code
I've organised the code as follows:
/src
/src/index.ts
/src/createUid.ts
createUid is the first function that I was putting into this lib. I'd like to separate each function into its own file (but if they must all be in one file, that's fine too).
createUid.ts
import { now, random, padStart } from "lodash"
/**
* Return a 16-digit unique integer based on the current time (ms) appended
* with a three-digit random or provided number ("counter").
*
* The id is an integer and consists of two parts:
* 1) The number of miliseconds is a 13-digit number
* 2) Appended with a three digit number, that is either:
* a) a left-padded number, if provided to the function
* b) a random numer
*
* 1675246953915 February 1st, 2023 (today)
* 9999999999999 November 20th, 2286
* 9007199254740 June 5th, 2255
* 9007199254740991 Max. safe integer
*
* Note:
* - This function won't work after November, 2286.
* If it is still in use then consider adding two instead of three digits,
* or use a bigint.
*
*/
const createUid = (counter?: number): (number|undefined) => {
let p1 = now() // ms
let p2 = ""
if (counter == undefined) {
p2 = padStart(random(0,999).toString(), 3, '0')
} else if (isNaN(counter)) {
p2 = padStart(random(0,999).toString(), 3, '0')
} else {
let carry = 0
if (counter > 999) {
counter = counter % 1000
carry = Math.trunc(counter / 1000)
}
p2 = padStart(counter.toString(),3,'0')
if (carry > 0) {
p1 += carry
}
}
// Create the integer
const retVal = parseInt(`${p1}${p2}`)
// Check if safe
if (!Number.isSafeInteger(retVal)) {
console.error(`Generated id is larger than safe integer ${Number.MAX_SAFE_INTEGER}.`)
return
}
return retVal
}
export { createUid }
index.ts
export { createUid } from './createUid'
I found two three possible working scenarios:
vitest
instead. Vitest is largely compatible with the Jest api. After installing Vitest, everything simply worked, without any configuration. The only thing to do is to add import { assert, expect, test } from 'vitest'
to your tests. If you have just started, and only have simple tests, they simply worked without any changes (YMMV).
In addition, both lodash and lodash-es worked fine.A better answer would include a scenario that uses "plain" lodash, or explains why lodash-es is actually a good thing. From what I'm reading in various posts is that lodash-es has several drawbacks (results in a larger bundle size, chains don't work).
Here is a working setup with (I think) sensible defaults. This library is for internal use, and I run Node 18, so I use a high target version here.
package.json
{
"name": "@vexna/util",
"version": "1.0.0",
"description": "Generic utilities, uses lodash",
"private": true,
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"sideEffects": false,
"scripts": {
"build": "rimraf dist && tsc && rollup -c rollup.config.js",
"test": "node test/spec",
"pretest": "npm run build"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@rollup/plugin-typescript": "^11.0.0",
"@types/lodash-es": "^4.17.6",
"lodash-es": "^4.17.21",
"rimraf": "^4.1.2",
"rollup": "^3.12.1",
"typescript": "^4.9.5"
},
"files": [
"dist"
],
"peerDependencies": {
"lodash": "^4.17.21"
}
}
tsconfig.ts
{
"compilerOptions": {
"module": "es2022",
"moduleResolution": "node",
"outDir": "dist",
"declaration": true,
"sourceMap": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"lib": ["es2022", "DOM", "DOM.Iterable"],
"target": "es2022",
"skipLibCheck": true,
"strict": true,
"exactOptionalPropertyTypes": true,
"noImplicitAny": true,
"noImplicitThis": true,
"checkJs": true
},
"include": ["./src/**/*.ts"]
}
rollup.config.js
import typescript from '@rollup/plugin-typescript'
const input = ["src/index.ts"]
export default [
{
input,
plugins: [
typescript()],
output: [
{
dir: "dist",
sourcemap: true,
}
],
external: ['lodash-es'],
}
]
The output has lodash entirely externalized, so very small bundle! As a precaution, I added "lodash": "^4.17.21" to the "peerDependencies". This is a little odd cause it's a mismatch with devDependencies (which uses the -es), but this seems fine. YMMV.
In the library code, you can import the lodash functions like so:
import { now, random, padStart } from "lodash-es"
A project that consumes this library must have lodash, and can import like so:
import { createUid } from '@vexna/util'