Skip to content

Commit 8a30ce6

Browse files
committed
fix(core): yarn 4 support
Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
1 parent e45076a commit 8a30ce6

5 files changed

Lines changed: 109 additions & 31 deletions

File tree

.yarnrc

Lines changed: 0 additions & 1 deletion
This file was deleted.

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,37 @@ pnpm ts-dev-tools install
102102

103103
Contributions, issues and feature requests are welcome!<br />Feel free to check [issues page](https://github.com/escemi-tech/ts-dev-tools/issues). You can also take a look at the [contributing guide](CONTRIBUTING) and [Contributor Code of Conduct](CODE-OF-CONDUCT.md).
104104

105+
### Developer setup
106+
107+
```sh
108+
npm install
109+
```
110+
111+
### Checks
112+
113+
```sh
114+
npm run lint
115+
npm run test
116+
npm run build
117+
```
118+
119+
Or run everything in one command:
120+
121+
```sh
122+
npm run ci
123+
```
124+
125+
### Clean cache
126+
127+
For some reason, if you need to clean cache, you can run the following commands:
128+
129+
```sh
130+
# Test cache
131+
rm -rf node_modules/.cache/ts-dev-tools
132+
# NX cache
133+
npx nx reset
134+
```
135+
105136
## Show your support
106137

107138
Give a ⭐️ if this project helped you!

packages/core/src/bin.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe("bin", () => {
1313
it("should display version", async () => {
1414
const result = await execBin("--version");
1515
expect(result.stderr).toBeFalsy();
16-
expect(result.stdout).toMatch(/[0-9]{1}\.[0-9]{1}\.[0-9]{1}/);
16+
expect(result.stdout).toMatch(/[0-9]+\.[0-9]+\.[0-9]+/);
1717
expect(result.code).toBe(0);
1818
}, 10000);
1919

packages/core/src/services/SymlinkDependenciesService.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ export class SymlinkDependenciesService {
3030

3131
const pluginDependenciesPath = await SymlinkDependenciesService.getPluginDependenciesPath(
3232
absoluteProjectDir,
33-
plugin
33+
plugin,
34+
pluginDependencies
3435
);
3536

3637
if (projectDependencyPath === pluginDependenciesPath) {
@@ -67,15 +68,22 @@ export class SymlinkDependenciesService {
6768

6869
private static async getPluginDependenciesPath(
6970
absoluteProjectDir: string,
70-
plugin: Plugin
71+
plugin: Plugin,
72+
pluginDependencies: string[]
7173
): Promise<string> {
7274
const pluginDependenciesPath = join(
7375
plugin.path,
7476
SymlinkDependenciesService.DEPENDENCIES_FOLDER
7577
);
7678

7779
if (existsSync(pluginDependenciesPath)) {
78-
return pluginDependenciesPath;
80+
const hasAnyPluginDependency = pluginDependencies.some((pluginDependency) =>
81+
existsSync(join(pluginDependenciesPath, pluginDependency))
82+
);
83+
84+
if (hasAnyPluginDependency) {
85+
return pluginDependenciesPath;
86+
}
7987
}
8088

8189
return await PackageManagerService.getNodeModulesPath(absoluteProjectDir);

packages/core/src/services/package-manager/YarnPackageManagerAdapter.ts

Lines changed: 66 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,35 @@ export class YarnPackageManagerAdapter extends AbstractPackageManagerAdapter {
3939
["yarn", "workspaces", primary, "--json"],
4040
["yarn", "workspaces", fallback, "--json"],
4141
];
42+
let lastOutput: string | undefined;
43+
let hasRecognizedOutput = false;
44+
let hasSuccessfulCommand = false;
4245

4346
for (const args of yarnCommands) {
4447
try {
4548
const output = await this.execCommand(args, dirPath, true);
46-
if (this.yarnWorkspacesOutputHasEntries(output)) {
49+
hasSuccessfulCommand = true;
50+
lastOutput = output;
51+
52+
const analysis = this.analyzeYarnWorkspacesOutput(output);
53+
if (analysis.hasEntries) {
4754
return true;
4855
}
56+
57+
if (analysis.hasRecognizedData) {
58+
hasRecognizedOutput = true;
59+
}
4960
} catch {
5061
// Try next command
5162
}
5263
}
5364

65+
if (hasSuccessfulCommand && !hasRecognizedOutput) {
66+
throw new Error(
67+
`Unexpected output from "yarn workspaces": ${lastOutput ?? "<empty>"}`
68+
);
69+
}
70+
5471
return false;
5572
}
5673

@@ -74,28 +91,46 @@ export class YarnPackageManagerAdapter extends AbstractPackageManagerAdapter {
7491
} catch {
7592
return false;
7693
}
94+
7795
}
7896

7997
async getNodeModulesPath(dirPath: string): Promise<string> {
8098
return join(dirPath, "node_modules");
8199
}
82100

83-
private yarnWorkspacesOutputHasEntries(output: string): boolean {
101+
private analyzeYarnWorkspacesOutput(
102+
output: string
103+
): { hasEntries: boolean; hasRecognizedData: boolean } {
84104
const entries = this.parseJsonLines(output);
85105

106+
if (entries.length === 0) {
107+
return { hasEntries: false, hasRecognizedData: false };
108+
}
109+
86110
let workspaceListCount = 0;
111+
let hasRecognizedData = false;
87112

88113
for (const entry of entries) {
89114
if (!entry || typeof entry !== "object") {
90115
continue;
91116
}
92117

118+
const type = (entry as { type?: string }).type;
119+
if (type === "error" || type === "warning") {
120+
continue;
121+
}
122+
93123
const data = (entry as { data?: unknown }).data ?? entry;
94124

95125
const parsedData = this.parseMaybeJsonString(data);
96126
if (parsedData && typeof parsedData === "object") {
127+
hasRecognizedData = true;
97128
if (this.hasWorkspaceMap(parsedData)) {
98-
return true;
129+
return { hasEntries: true, hasRecognizedData };
130+
}
131+
132+
if (this.isWorkspaceInfoMap(parsedData)) {
133+
return { hasEntries: true, hasRecognizedData };
99134
}
100135

101136
if (this.isWorkspaceListEntry(parsedData)) {
@@ -105,15 +140,9 @@ export class YarnPackageManagerAdapter extends AbstractPackageManagerAdapter {
105140
}
106141

107142
if (workspaceListCount > 0) {
108-
return true;
143+
return { hasEntries: true, hasRecognizedData };
109144
}
110-
111-
const parsedOutput = this.parseJsonObjectFromOutput(output);
112-
if (parsedOutput && typeof parsedOutput === "object") {
113-
return Object.keys(parsedOutput as Record<string, unknown>).length > 0;
114-
}
115-
116-
return false;
145+
return { hasEntries: false, hasRecognizedData };
117146
}
118147

119148
private parseMaybeJsonString(value: unknown): unknown {
@@ -137,29 +166,33 @@ export class YarnPackageManagerAdapter extends AbstractPackageManagerAdapter {
137166
return !!workspaces && Object.keys(workspaces).length > 0;
138167
}
139168

140-
private parseJsonObjectFromOutput(output: string): unknown {
141-
const start = output.indexOf("{");
142-
const end = output.lastIndexOf("}");
143-
144-
if (start < 0 || end <= start) {
145-
return undefined;
169+
private isWorkspaceListEntry(value: unknown): boolean {
170+
if (!value || typeof value !== "object") {
171+
return false;
146172
}
147173

148-
const slice = output.slice(start, end + 1).trim();
149-
try {
150-
return JSON.parse(slice) as unknown;
151-
} catch {
152-
return undefined;
153-
}
174+
const entry = value as { name?: string; location?: string };
175+
return typeof entry.name === "string" && typeof entry.location === "string" && entry.location !== ".";
154176
}
155177

156-
private isWorkspaceListEntry(value: unknown): boolean {
178+
private isWorkspaceInfoMap(value: unknown): boolean {
157179
if (!value || typeof value !== "object") {
158180
return false;
159181
}
160182

161-
const entry = value as { name?: string; location?: string };
162-
return typeof entry.name === "string" && typeof entry.location === "string" && entry.location !== ".";
183+
const entries = Object.values(value as Record<string, unknown>);
184+
if (entries.length === 0) {
185+
return false;
186+
}
187+
188+
return entries.some((entry) => {
189+
if (!entry || typeof entry !== "object") {
190+
return false;
191+
}
192+
193+
const location = (entry as { location?: unknown }).location;
194+
return typeof location === "string" && location.length > 0 && location !== ".";
195+
});
163196
}
164197

165198
private yarnListOutputHasPackage(output: string, packageName: string): boolean {
@@ -174,6 +207,13 @@ export class YarnPackageManagerAdapter extends AbstractPackageManagerAdapter {
174207
const trees = data?.trees ?? (entry as { trees?: Array<{ name: string }> }).trees;
175208

176209
if (!trees) {
210+
const children = (entry as { children?: Record<string, unknown> }).children;
211+
if (children) {
212+
const childKeys = Object.keys(children);
213+
if (childKeys.some((key) => key.startsWith(packageName + "@"))) {
214+
return true;
215+
}
216+
}
177217
continue;
178218
}
179219

0 commit comments

Comments
 (0)