Skip to content

Commit 42c1287

Browse files
committed
feat: add support for pnpm
Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
1 parent 245d68a commit 42c1287

File tree

7 files changed

+151
-37
lines changed

7 files changed

+151
-37
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
## Opinionated and advisable packages to configure tools to develop a Typescript project
1616

17+
> ✨ Works with npm, yarn, and pnpm package managers
18+
1719
---
1820

1921
## Why **ts-dev-tools** ?

packages/core/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
- Install and configure [eslint](https://eslint.org/) with recommended [rules](https://eslint.org/docs/rules/)
3939
- Configure Eslint to works with [typescript](https://github.com/typescript-eslint/typescript-eslint#readme)
4040
- Install and configure the following Eslint plugins:
41-
4241
- [eslint-plugin-import](https://github.com/benmosher/eslint-plugin-import): ES2015+ (ES6+) import/export syntax
4342
- [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest#readme): Rules for Jest
4443
- [eslint-plugin-node](https://github.com/mysticatea/eslint-plugin-node#readme): Additional ESLint's rules for Node.js
@@ -74,13 +73,13 @@ npm install --save-dev @ts-dev-tools/core
7473
Or
7574

7675
```sh
77-
yarn add --dev @ts-dev-tools/core
76+
pnpm add -D @ts-dev-tools/core
7877
```
7978

8079
Or
8180

8281
```sh
83-
pnpm add -D @ts-dev-tools/core
82+
yarn add --dev @ts-dev-tools/core
8483
```
8584

8685
### _2_. Enable ts-dev-tools
@@ -92,13 +91,13 @@ npm exec ts-dev-tools install
9291
Or
9392

9493
```sh
95-
yarn ts-dev-tools install
94+
pnpm ts-dev-tools install
9695
```
9796

9897
Or
9998

10099
```sh
101-
pnpm ts-dev-tools install
100+
yarn ts-dev-tools install
102101
```
103102

104103
⚠️ If your package is using yarn, is not private and you're publishing it on a registry like npmjs.com, you need to disable postinstall script using [pinst](https://github.com/typicode/pinst). Otherwise, postinstall will run when someone installs your package and result in an error.

packages/core/__tests__/test-project/package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/src/services/PackageManagerService.spec.ts

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { existsSync, writeFileSync } from "fs";
12
import { safeExec } from "../tests/cli";
23
import { createProjectForTestFile, deleteTestProject } from "../tests/test-project";
34
import { PackageJson } from "./PackageJson";
@@ -31,14 +32,14 @@ describe("PackageManagerService", () => {
3132
}
3233
});
3334

34-
it("should retrieve the default package manager when no one is detectable", () => {
35-
const packageManager = PackageManagerService.detectPackageManager(testProjectDir);
36-
37-
expect(packageManager).toEqual(PackageManagerType.npm);
35+
it("should throws an error when no package manager is detectable", () => {
36+
expect(() => {
37+
PackageManagerService.detectPackageManager(testProjectDir);
38+
}).toThrow(`Could not detect package manager in directory: ${testProjectDir}. No lock file found.`);
3839
});
3940
});
4041

41-
describe.each([PackageManagerType.npm, PackageManagerType.yarn])(
42+
describe.each([PackageManagerType.npm, PackageManagerType.yarn, PackageManagerType.pnpm])(
4243
`with package manager %s`,
4344
(packageManagerType) => {
4445
const packageTypeTestFileName = __filename.replace(
@@ -48,8 +49,23 @@ describe("PackageManagerService", () => {
4849

4950
beforeEach(async () => {
5051
testProjectDir = await createProjectForTestFile(packageTypeTestFileName, useCache);
51-
await safeExec(testProjectDir, `${packageManagerType} init --yes`);
52-
await safeExec(testProjectDir, `${packageManagerType} install --silent`);
52+
53+
if (packageManagerType === PackageManagerType.pnpm) {
54+
// pnpm init fails if package.json already exists, so we need to handle this differently
55+
const packageJsonPath = `${testProjectDir}/package.json`;
56+
if (!existsSync(packageJsonPath)) {
57+
await safeExec(testProjectDir, `${packageManagerType} init`);
58+
}
59+
// Create pnpm-lock.yaml to indicate pnpm usage
60+
writeFileSync(`${testProjectDir}/pnpm-lock.yaml`, 'lockfileVersion: \'6.0\'\n');
61+
} else {
62+
await safeExec(testProjectDir, `${packageManagerType} init --yes`);
63+
}
64+
65+
const installCommand = packageManagerType === PackageManagerType.npm
66+
? `${packageManagerType} install --silent`
67+
: `${packageManagerType} install`;
68+
await safeExec(testProjectDir, installCommand);
5369
});
5470

5571
afterEach(async () => {
@@ -104,7 +120,18 @@ describe("PackageManagerService", () => {
104120
const testPackageDir = `${testProjectDir}/packages/test-package`;
105121

106122
await safeExec(testProjectDir, `mkdir -p ${testPackageDir}`);
107-
await safeExec(testPackageDir, `${packageManagerType} init --yes`);
123+
124+
if (packageManagerType === PackageManagerType.pnpm) {
125+
// For pnpm, also create a pnpm-workspace.yaml file
126+
writeFileSync(`${testProjectDir}/pnpm-workspace.yaml`, 'packages:\n - "packages/*"\n');
127+
const packageJsonPath = `${testPackageDir}/package.json`;
128+
if (!existsSync(packageJsonPath)) {
129+
await safeExec(testPackageDir, `${packageManagerType} init`);
130+
}
131+
} else {
132+
await safeExec(testPackageDir, `${packageManagerType} init --yes`);
133+
}
134+
108135
await safeExec(testProjectDir, `${packageManagerType} install`);
109136

110137
const isMonorepo = await PackageManagerService.isMonorepo(testProjectDir);

packages/core/src/services/PackageManagerService.ts

Lines changed: 95 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,28 @@ import { spawn } from "child_process";
22
import { existsSync } from "fs";
33
import { join } from "path";
44

5+
import { PackageJson } from "./PackageJson";
6+
57
export enum PackageManagerType {
68
yarn = "yarn",
79
npm = "npm",
10+
pnpm = "pnpm",
811
}
912

1013
export class PackageManagerService {
1114
static detectPackageManager(dirPath: string): PackageManagerType {
15+
if (existsSync(join(dirPath, "pnpm-lock.yaml"))) {
16+
return PackageManagerType.pnpm;
17+
}
1218
if (existsSync(join(dirPath, "yarn.lock"))) {
1319
return PackageManagerType.yarn;
1420
}
15-
return PackageManagerType.npm;
21+
22+
if (existsSync(join(dirPath, "package-lock.json"))) {
23+
return PackageManagerType.npm;
24+
}
25+
26+
throw new Error(`Could not detect package manager in directory: ${dirPath}. No lock file found.`);
1627
}
1728

1829
static async addDevPackage(packageName: string, dirPath: string): Promise<void> {
@@ -37,6 +48,13 @@ export class PackageManagerService {
3748
args.push("--no-workspaces");
3849
}
3950
break;
51+
case PackageManagerType.pnpm:
52+
args.push("add", "--save-dev");
53+
54+
if (isMonorepo) {
55+
args.push("--workspace-root");
56+
}
57+
break;
4058
}
4159

4260
args.push(packageName);
@@ -47,37 +65,69 @@ export class PackageManagerService {
4765
static async isMonorepo(dirPath: string) {
4866
const packageManager = PackageManagerService.detectPackageManager(dirPath);
4967

50-
const args: string[] = [packageManager];
51-
5268
switch (packageManager) {
53-
case PackageManagerType.yarn:
54-
args.push("workspaces", "info");
55-
break;
56-
57-
case PackageManagerType.npm:
58-
args.push("--workspaces", "list");
59-
break;
60-
}
61-
62-
args.push("> /dev/null 2>&1 && echo true || echo false;");
69+
case PackageManagerType.yarn: {
70+
const args = [packageManager, "workspaces", "info"];
71+
args.push("> /dev/null 2>&1 && echo true || echo false;");
72+
const output = await PackageManagerService.execCommand(args, dirPath, true);
73+
return output.trim() === "true";
74+
}
6375

64-
const output = await PackageManagerService.execCommand(args, dirPath, true);
76+
case PackageManagerType.npm: {
77+
const args = [packageManager, "--workspaces", "list"];
78+
args.push("> /dev/null 2>&1 && echo true || echo false;");
79+
const output = await PackageManagerService.execCommand(args, dirPath, true);
80+
return output.trim() === "true";
81+
}
6582

66-
return output.trim() === "true";
83+
case PackageManagerType.pnpm: {
84+
// For pnpm, check if pnpm-workspace.yaml exists or if workspaces are defined in package.json
85+
const pnpmWorkspaceFile = join(dirPath, "pnpm-workspace.yaml");
86+
if (existsSync(pnpmWorkspaceFile)) {
87+
return true;
88+
}
89+
// Check package.json for workspace field (though pnpm prefers pnpm-workspace.yaml)
90+
try {
91+
const packageJson = PackageJson.fromDirPath(dirPath);
92+
const content = packageJson.getContent();
93+
return !!(content.workspaces);
94+
} catch {
95+
return false;
96+
}
97+
}
98+
}
6799
}
68100

69101
static async isPackageInstalled(packageName: string, dirPath: string): Promise<boolean> {
70102
const packageManager = PackageManagerService.detectPackageManager(dirPath);
71103

72-
const args = [
73-
packageManager,
74-
"list",
75-
"--depth=1",
76-
"--json",
77-
"--no-progress",
78-
`--pattern="${packageName}"`,
79-
"--non-interactive",
80-
];
104+
let args: string[];
105+
106+
switch (packageManager) {
107+
case PackageManagerType.yarn:
108+
case PackageManagerType.npm:
109+
args = [
110+
packageManager,
111+
"list",
112+
"--depth=1",
113+
"--json",
114+
"--no-progress",
115+
`--pattern="${packageName}"`,
116+
"--non-interactive",
117+
];
118+
break;
119+
case PackageManagerType.pnpm:
120+
args = [
121+
packageManager,
122+
"list",
123+
packageName,
124+
"--json",
125+
"--depth=1",
126+
];
127+
break;
128+
default:
129+
throw new Error(`Unsupported package manager: ${packageManager}`);
130+
}
81131

82132
const output = await PackageManagerService.execCommand(args, dirPath, true);
83133

@@ -92,6 +142,16 @@ export class PackageManagerService {
92142
return installedPackages.dependencies
93143
? Object.prototype.hasOwnProperty.call(installedPackages.dependencies, packageName)
94144
: false;
145+
case PackageManagerType.pnpm:
146+
// pnpm returns an array of package objects
147+
if (Array.isArray(installedPackages) && installedPackages.length > 0) {
148+
const pkg = installedPackages[0];
149+
return (pkg.dependencies && Object.prototype.hasOwnProperty.call(pkg.dependencies, packageName)) ||
150+
(pkg.devDependencies && Object.prototype.hasOwnProperty.call(pkg.devDependencies, packageName));
151+
}
152+
return false;
153+
default:
154+
throw new Error(`Unsupported package manager: ${packageManager}`);
95155
}
96156
}
97157

@@ -112,6 +172,17 @@ export class PackageManagerService {
112172
)
113173
).trim();
114174
break;
175+
case PackageManagerType.pnpm:
176+
nodeModulesPath = (
177+
await PackageManagerService.execCommand(
178+
[packageManager, "root"],
179+
dirPath,
180+
true
181+
)
182+
).trim();
183+
break;
184+
default:
185+
throw new Error(`Unsupported package manager: ${packageManager}`);
115186
}
116187

117188
if (nodeModulesPath) {

packages/core/src/tests/test-project.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const rootDirPath = resolve(__dirname, "../../../..");
99
const corePackageDirPath = resolve(__dirname, "..", "..");
1010
const testProjectDir = resolve("__tests__/test-project");
1111
const defaultPackageJsonPath = join(testProjectDir, "package.json");
12+
const defaultPackageJsonLockPath = join(testProjectDir, "package-lock.json");
1213

1314
export const getPackageNameFromFilepath = (filepath: string): string => {
1415
const relativeFilepath = relative(rootDirPath, filepath);
@@ -45,6 +46,7 @@ async function defaultProjectGenerator(testProjectDirPath: string): Promise<void
4546
symlinkSync(resolve(__dirname, ".."), tsDevToolsDistPath);
4647

4748
copyFileSync(defaultPackageJsonPath, join(testProjectDirPath, "package.json"));
49+
copyFileSync(defaultPackageJsonLockPath, join(testProjectDirPath, "package-lock.json"));
4850
}
4951

5052
export type TestProjectGenerator = (testProjectDir: string) => Promise<void>;

packages/react/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ npm install --save-dev @ts-dev-tools/react
4242

4343
Or
4444

45+
```sh
46+
pnpm add --save-dev @ts-dev-tools/react
47+
```
48+
49+
Or
50+
4551
```sh
4652
yarn add --dev @ts-dev-tools/react
4753
```
@@ -54,6 +60,12 @@ npm exec ts-dev-tools install
5460

5561
Or
5662

63+
```sh
64+
pnpm ts-dev-tools install
65+
```
66+
67+
Or
68+
5769
```sh
5870
yarn ts-dev-tools install
5971
```

0 commit comments

Comments
 (0)