Skip to content

Commit c052b8c

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

5 files changed

Lines changed: 135 additions & 30 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: 92 additions & 25 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,53 @@ 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 {
84-
const entries = this.parseJsonLines(output);
101+
private analyzeYarnWorkspacesOutput(
102+
output: string
103+
): { hasEntries: boolean; hasRecognizedData: boolean } {
104+
let entries = this.parseJsonLines(output);
105+
106+
if (entries.length === 0) {
107+
const jsonBlock = this.extractJsonBlock(output);
108+
if (jsonBlock && typeof jsonBlock === "object") {
109+
entries = [jsonBlock];
110+
}
111+
}
112+
113+
if (entries.length === 0) {
114+
return { hasEntries: false, hasRecognizedData: false };
115+
}
85116

86117
let workspaceListCount = 0;
118+
let hasRecognizedData = false;
87119

88120
for (const entry of entries) {
89121
if (!entry || typeof entry !== "object") {
90122
continue;
91123
}
92124

125+
const type = (entry as { type?: string }).type;
126+
if (type === "error" || type === "warning") {
127+
continue;
128+
}
129+
93130
const data = (entry as { data?: unknown }).data ?? entry;
94131

95132
const parsedData = this.parseMaybeJsonString(data);
96133
if (parsedData && typeof parsedData === "object") {
134+
hasRecognizedData = true;
97135
if (this.hasWorkspaceMap(parsedData)) {
98-
return true;
136+
return { hasEntries: true, hasRecognizedData };
137+
}
138+
139+
if (this.isWorkspaceInfoMap(parsedData)) {
140+
return { hasEntries: true, hasRecognizedData };
99141
}
100142

101143
if (this.isWorkspaceListEntry(parsedData)) {
@@ -105,15 +147,9 @@ export class YarnPackageManagerAdapter extends AbstractPackageManagerAdapter {
105147
}
106148

107149
if (workspaceListCount > 0) {
108-
return true;
150+
return { hasEntries: true, hasRecognizedData };
109151
}
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;
152+
return { hasEntries: false, hasRecognizedData };
117153
}
118154

119155
private parseMaybeJsonString(value: unknown): unknown {
@@ -128,31 +164,35 @@ export class YarnPackageManagerAdapter extends AbstractPackageManagerAdapter {
128164
}
129165
}
130166

131-
private hasWorkspaceMap(value: unknown): boolean {
132-
if (!value || typeof value !== "object") {
133-
return false;
134-
}
135-
136-
const workspaces = (value as { workspaces?: Record<string, unknown> }).workspaces;
137-
return !!workspaces && Object.keys(workspaces).length > 0;
138-
}
139-
140-
private parseJsonObjectFromOutput(output: string): unknown {
167+
private extractJsonBlock(output: string): unknown {
141168
const start = output.indexOf("{");
142169
const end = output.lastIndexOf("}");
143170

144-
if (start < 0 || end <= start) {
171+
if (start === -1 || end === -1 || end <= start) {
172+
return undefined;
173+
}
174+
175+
const candidate = output.slice(start, end + 1).trim();
176+
if (candidate.length === 0) {
145177
return undefined;
146178
}
147179

148-
const slice = output.slice(start, end + 1).trim();
149180
try {
150-
return JSON.parse(slice) as unknown;
181+
return JSON.parse(candidate) as unknown;
151182
} catch {
152183
return undefined;
153184
}
154185
}
155186

187+
private hasWorkspaceMap(value: unknown): boolean {
188+
if (!value || typeof value !== "object") {
189+
return false;
190+
}
191+
192+
const workspaces = (value as { workspaces?: Record<string, unknown> }).workspaces;
193+
return !!workspaces && Object.keys(workspaces).length > 0;
194+
}
195+
156196
private isWorkspaceListEntry(value: unknown): boolean {
157197
if (!value || typeof value !== "object") {
158198
return false;
@@ -162,6 +202,26 @@ export class YarnPackageManagerAdapter extends AbstractPackageManagerAdapter {
162202
return typeof entry.name === "string" && typeof entry.location === "string" && entry.location !== ".";
163203
}
164204

205+
private isWorkspaceInfoMap(value: unknown): boolean {
206+
if (!value || typeof value !== "object") {
207+
return false;
208+
}
209+
210+
const entries = Object.values(value as Record<string, unknown>);
211+
if (entries.length === 0) {
212+
return false;
213+
}
214+
215+
return entries.some((entry) => {
216+
if (!entry || typeof entry !== "object") {
217+
return false;
218+
}
219+
220+
const location = (entry as { location?: unknown }).location;
221+
return typeof location === "string" && location.length > 0 && location !== ".";
222+
});
223+
}
224+
165225
private yarnListOutputHasPackage(output: string, packageName: string): boolean {
166226
const entries = this.parseJsonLines(output);
167227

@@ -174,6 +234,13 @@ export class YarnPackageManagerAdapter extends AbstractPackageManagerAdapter {
174234
const trees = data?.trees ?? (entry as { trees?: Array<{ name: string }> }).trees;
175235

176236
if (!trees) {
237+
const children = (entry as { children?: Record<string, unknown> }).children;
238+
if (children) {
239+
const childKeys = Object.keys(children);
240+
if (childKeys.some((key) => key.startsWith(packageName + "@"))) {
241+
return true;
242+
}
243+
}
177244
continue;
178245
}
179246

0 commit comments

Comments
 (0)