Skip to content

Commit 2ab1867

Browse files
committed
feat(plugin): ✨ add functionality to register new plugins in workspace
1 parent a27322e commit 2ab1867

File tree

2 files changed

+161
-0
lines changed

2 files changed

+161
-0
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { readdir, readFile, writeFile } from "fs/promises";
2+
import { join } from "path";
3+
4+
import type { PackageJSON } from "../../helpers/packages-json.js";
5+
6+
const writeJson = async (path: string, data: unknown) =>
7+
writeFile(path, JSON.stringify(data, null, 2) + "\n");
8+
9+
interface AddPluginToWorkspaceArgs {
10+
packageManager: string;
11+
pluginName: string;
12+
pluginPath: string;
13+
rootPath: string;
14+
}
15+
16+
/**
17+
* Recursively find all package.json files in a directory
18+
* @param dir - The directory to search in
19+
* @param results - Array to accumulate results (used internally)
20+
* @returns Array of absolute paths to package.json files
21+
*/
22+
const findPackageJsonFiles = async (
23+
dir: string,
24+
results: string[] = [],
25+
): Promise<string[]> => {
26+
try {
27+
const entries = await readdir(dir, { withFileTypes: true });
28+
29+
for (const entry of entries) {
30+
const fullPath = join(dir, entry.name);
31+
32+
// Skip node_modules and dist folders
33+
if (entry.name === "node_modules" || entry.name === "dist") {
34+
continue;
35+
}
36+
37+
if (entry.isDirectory()) {
38+
await findPackageJsonFiles(fullPath, results);
39+
} else if (entry.name === "package.json") {
40+
results.push(fullPath);
41+
}
42+
}
43+
} catch {
44+
// Ignore permission errors or inaccessible directories
45+
}
46+
47+
return results;
48+
};
49+
50+
/**
51+
* Adds the newly created plugin to all workspace packages that depend on @vitnode/core.
52+
* This function:
53+
* 1. Finds all package.json files in the workspace (excluding node_modules and dist)
54+
* 2. Identifies packages that have @vitnode/core as a dependency
55+
* 3. Adds the new plugin as a dependency with the appropriate workspace reference
56+
* 4. Skips packages in the plugins folder (except the new plugin itself)
57+
* 5. Respects the package manager's workspace protocol
58+
*
59+
* @param packageManager - The package manager being used (pnpm, npm, yarn, bun)
60+
* @param pluginName - The name of the plugin to add (e.g., "@my-org/my-plugin")
61+
* @param pluginPath - The absolute path to the plugin directory
62+
* @param rootPath - The absolute path to the monorepo root (where turbo.json is located)
63+
*/
64+
65+
export const addPluginToWorkspace = async ({
66+
packageManager,
67+
pluginName,
68+
pluginPath,
69+
rootPath,
70+
}: AddPluginToWorkspaceArgs) => {
71+
// Find all package.json files in the workspace
72+
const packageJsonFiles = await findPackageJsonFiles(rootPath);
73+
74+
for (const packageJsonPath of packageJsonFiles) {
75+
// Skip if this is the plugin's own package.json
76+
if (packageJsonPath === join(pluginPath, "package.json")) {
77+
continue;
78+
}
79+
80+
// Skip if this is in the plugins folder (excluding the new plugin itself)
81+
if (
82+
packageJsonPath.includes("/plugins/") &&
83+
!packageJsonPath.startsWith(pluginPath)
84+
) {
85+
continue;
86+
}
87+
88+
try {
89+
const content = await readFile(packageJsonPath, "utf-8");
90+
const pkg: PackageJSON = JSON.parse(content);
91+
92+
// Check if this package has @vitnode/core
93+
const hasVitnodeCore =
94+
pkg.dependencies?.["@vitnode/core"] ??
95+
pkg.devDependencies?.["@vitnode/core"];
96+
97+
if (!hasVitnodeCore) {
98+
continue;
99+
}
100+
101+
// Determine the workspace reference based on package manager
102+
let workspaceReference: string;
103+
104+
switch (packageManager) {
105+
case "bun":
106+
workspaceReference = "workspace:*";
107+
break;
108+
case "npm":
109+
workspaceReference = "*";
110+
break;
111+
case "pnpm":
112+
workspaceReference = "workspace:*";
113+
break;
114+
case "yarn":
115+
workspaceReference = "workspace:*";
116+
break;
117+
default:
118+
workspaceReference = "workspace:*";
119+
}
120+
121+
// Add the plugin to dependencies
122+
pkg.dependencies ??= {};
123+
124+
pkg.dependencies[pluginName] = workspaceReference;
125+
126+
// Sort dependencies alphabetically
127+
pkg.dependencies = Object.keys(pkg.dependencies)
128+
.sort()
129+
.reduce<Record<string, string>>((acc, key) => {
130+
acc[key] = pkg.dependencies?.[key] ?? "";
131+
132+
return acc;
133+
}, {});
134+
135+
await writeJson(packageJsonPath, pkg);
136+
} catch {
137+
// Skip files that can't be read or parsed
138+
continue;
139+
}
140+
}
141+
};

packages/create-vitnode-app/src/plugin/create/create-plugin-vitnode.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { CreatePluginCliReturn } from "../questions.js";
1010
import { getPackageManagerFromRoot } from "../../helpers/get-package-manager-from-root.js";
1111
import { installDependencies } from "../../helpers/install-dependencies.js";
1212
import { isFolderEmpty } from "../../helpers/is-folder-empty.js";
13+
import { addPluginToWorkspace } from "./add-plugin-to-workspace.js";
1314
import { createPluginPackageJSON } from "./create-package-json.js";
1415

1516
export const createPluginVitNode = async ({
@@ -93,6 +94,25 @@ export const createPluginVitNode = async ({
9394
await cp(templateEslintPath, pluginPath, { recursive: true });
9495
}
9596

97+
// Find the root of the monorepo (where turbo.json is located)
98+
let rootPath = process.cwd();
99+
let currentDir = rootPath;
100+
while (currentDir !== dirname(currentDir)) {
101+
if (existsSync(join(currentDir, "turbo.json"))) {
102+
rootPath = currentDir;
103+
break;
104+
}
105+
currentDir = dirname(currentDir);
106+
}
107+
108+
spinner.text = "Adding plugin to workspace packages...";
109+
await addPluginToWorkspace({
110+
packageManager,
111+
pluginName,
112+
pluginPath,
113+
rootPath,
114+
});
115+
96116
if (install) {
97117
spinner.text = "Installing dependencies...";
98118
await installDependencies({

0 commit comments

Comments
 (0)