Tips and Tricks
Using the REMOVE Symbol
The REMOVE
symbol provides a clean way to remove entries, dependencies, scripts, and files across your monorepo. Import it from @monorepolint/rules
:
import { REMOVE } from "@monorepolint/rules";
Benefits of REMOVE
- Explicit Intent: Makes removal operations explicit and intentional
- Conditional Behavior: Only reports errors when items exist, avoiding noise
- Clean Configuration: More readable than legacy
undefined
approaches - Type Safety: Provides better TypeScript support
Supported Rules
The REMOVE
symbol works with these rules:
- fileContents: Remove files from packages
- packageScript: Remove scripts from package.json
- requireDependency: Remove dependencies from package.json
- packageEntry: Remove arbitrary fields from package.json
Migration Cleanup Example
Here's a comprehensive example showing how to use REMOVE
for migrating from Jest to Vitest:
import {
fileContents,
packageScript,
REMOVE,
requireDependency,
} from "@monorepolint/rules";
export default {
rules: [
// Remove Jest configuration files
fileContents({
options: {
file: "jest.config.js",
template: REMOVE,
},
}),
fileContents({
options: {
file: "jest.config.json",
template: REMOVE,
},
}),
// Remove Jest scripts
packageScript({
options: {
scripts: {
jest: REMOVE,
"jest:watch": REMOVE,
"test:jest": REMOVE,
},
},
}),
// Remove Jest dependencies
requireDependency({
options: {
devDependencies: {
jest: REMOVE,
"@types/jest": REMOVE,
"ts-jest": REMOVE,
"jest-environment-jsdom": REMOVE,
},
},
}),
// Add Vitest configuration and dependencies
fileContents({
options: {
file: "vitest.config.mjs",
templateFile: "./templates/vitest.config.mjs",
},
}),
packageScript({
options: {
scripts: {
test: "vitest run --passWithNoTests",
"test:watch": "vitest --passWithNoTests",
},
},
}),
requireDependency({
options: {
devDependencies: {
vitest: "^1.0.0",
"@vitest/ui": "^1.0.0",
},
},
}),
],
};
Standardizing package.json Exports
To maintain consistency across packages, it is recommended to define a standard for exports, such as mapping all files in the public/
directory as root exports. This can be achieved by using the following configuration:
packageEntry({
options: {
entries: {
exports: {
".": {
types: "./dist/index.d.ts",
import: "./dist/index.mjs",
require: "./dist/index.js",
},
"./*": {
types: "./dist/public/*.d.ts",
import: "./dist/public/*.mjs",
require: "./dist/public/*.js",
},
},
},
},
}),
This configuration can be combined with tools like tsup
to automatically bundle exports:
{
...
"entry": ["src/index.ts", "src/public/*.ts"]
...
}
Pre-formatting Generated Content with dprint
Pre-formatting generated content using dprint can be challenging since it cannot be used directly from node. However, it is possible to create wrappers by executing it in a shell:
const formatWithDprint = (contents, ext) => async (context) => {
const result = child_process.spawnSync(
`pnpm exec dprint fmt --stdin foo.${ext}`,
{
input: contents,
encoding: "utf8",
shell: true,
},
);
if (result.error) {
throw result.error;
}
return result.stdout;
};
By utilizing this wrapper, you can ensure that your files are properly formatted:
const tsupContents = formatWithDprint(
`
import { defineConfig } from "tsup";
export default defineConfig(async (options) =>
(await import("mytsup")).default(options)
);
`,
"js",
);
// ...
return [
fileContents({
...shared,
options: {
file: "tsup.config.js",
template: tsupContents,
},
}),
];