aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/turbo-workspaces/src/managers
diff options
context:
space:
mode:
author简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
committer简律纯 <hsiangnianian@outlook.com>2023-04-28 01:36:44 +0800
commitdd84b9d64fb98746a230cd24233ff50a562c39c9 (patch)
treeb583261ef00b3afe72ec4d6dacb31e57779a6faf /packages/turbo-workspaces/src/managers
parent0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff)
downloadHydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz
HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip
Diffstat (limited to 'packages/turbo-workspaces/src/managers')
-rw-r--r--packages/turbo-workspaces/src/managers/index.ts11
-rw-r--r--packages/turbo-workspaces/src/managers/npm.ts223
-rw-r--r--packages/turbo-workspaces/src/managers/pnpm.ts238
-rw-r--r--packages/turbo-workspaces/src/managers/yarn.ts222
4 files changed, 694 insertions, 0 deletions
diff --git a/packages/turbo-workspaces/src/managers/index.ts b/packages/turbo-workspaces/src/managers/index.ts
new file mode 100644
index 0000000..e026aed
--- /dev/null
+++ b/packages/turbo-workspaces/src/managers/index.ts
@@ -0,0 +1,11 @@
+import pnpm from "./pnpm";
+import npm from "./npm";
+import yarn from "./yarn";
+import { ManagerHandler, PackageManager } from "../types";
+
+const MANAGERS: Record<PackageManager, ManagerHandler> = {
+ pnpm,
+ yarn,
+ npm,
+};
+export default MANAGERS;
diff --git a/packages/turbo-workspaces/src/managers/npm.ts b/packages/turbo-workspaces/src/managers/npm.ts
new file mode 100644
index 0000000..26fd76a
--- /dev/null
+++ b/packages/turbo-workspaces/src/managers/npm.ts
@@ -0,0 +1,223 @@
+import fs from "fs-extra";
+import path from "path";
+import { ConvertError } from "../errors";
+import updateDependencies from "../updateDependencies";
+import {
+ DetectArgs,
+ ReadArgs,
+ CreateArgs,
+ RemoveArgs,
+ CleanArgs,
+ Project,
+ ConvertArgs,
+ ManagerHandler,
+} from "../types";
+import {
+ getMainStep,
+ getWorkspaceInfo,
+ getPackageJson,
+ expandWorkspaces,
+ getWorkspacePackageManager,
+ expandPaths,
+} from "../utils";
+
+/**
+ * Check if a given project is using npm workspaces
+ * Verify by checking for the existence of:
+ * 1. package-lock.json
+ * 2. packageManager field in package.json
+ */
+async function detect(args: DetectArgs): Promise<boolean> {
+ const lockFile = path.join(args.workspaceRoot, "package-lock.json");
+ const packageManager = getWorkspacePackageManager({
+ workspaceRoot: args.workspaceRoot,
+ });
+ return fs.existsSync(lockFile) || packageManager === "npm";
+}
+
+/**
+ Read workspace data from npm workspaces into generic format
+*/
+async function read(args: ReadArgs): Promise<Project> {
+ const isNpm = await detect(args);
+ if (!isNpm) {
+ throw new ConvertError("Not an npm project", {
+ type: "package_manager-unexpected",
+ });
+ }
+
+ const packageJson = getPackageJson(args);
+ const { name, description } = getWorkspaceInfo(args);
+ return {
+ name,
+ description,
+ packageManager: "npm",
+ paths: expandPaths({
+ root: args.workspaceRoot,
+ lockFile: "package-lock.json",
+ }),
+ workspaceData: {
+ globs: packageJson.workspaces || [],
+ workspaces: expandWorkspaces({
+ workspaceGlobs: packageJson.workspaces,
+ ...args,
+ }),
+ },
+ };
+}
+
+/**
+ * Create npm workspaces from generic format
+ *
+ * Creating npm workspaces involves:
+ * 1. Adding the workspaces field in package.json
+ * 2. Setting the packageManager field in package.json
+ * 3. Updating all workspace package.json dependencies to ensure correct format
+ */
+async function create(args: CreateArgs): Promise<void> {
+ const { project, options, to, logger } = args;
+ const hasWorkspaces = project.workspaceData.globs.length > 0;
+
+ logger.mainStep(
+ getMainStep({ packageManager: "npm", action: "create", project })
+ );
+ const packageJson = getPackageJson({ workspaceRoot: project.paths.root });
+ logger.rootHeader();
+
+ // package manager
+ logger.rootStep(
+ `adding "packageManager" field to ${path.relative(
+ project.paths.root,
+ project.paths.packageJson
+ )}`
+ );
+ packageJson.packageManager = `${to.name}@${to.version}`;
+
+ if (hasWorkspaces) {
+ // workspaces field
+ logger.rootStep(
+ `adding "workspaces" field to ${path.relative(
+ project.paths.root,
+ project.paths.packageJson
+ )}`
+ );
+ packageJson.workspaces = project.workspaceData.globs;
+
+ // write package.json here instead of deferring to avoid negating the changes made by updateDependencies
+ if (!options?.dry) {
+ fs.writeJSONSync(project.paths.packageJson, packageJson, { spaces: 2 });
+ }
+
+ // root dependencies
+ updateDependencies({
+ workspace: { name: "root", paths: project.paths },
+ project,
+ to,
+ logger,
+ options,
+ });
+
+ // workspace dependencies
+ logger.workspaceHeader();
+ project.workspaceData.workspaces.forEach((workspace) =>
+ updateDependencies({ workspace, project, to, logger, options })
+ );
+ } else {
+ if (!options?.dry) {
+ fs.writeJSONSync(project.paths.packageJson, packageJson, { spaces: 2 });
+ }
+ }
+}
+
+/**
+ * Remove npm workspace data
+ * Removing npm workspaces involves:
+ * 1. Removing the workspaces field from package.json
+ * 2. Removing the node_modules directory
+ */
+async function remove(args: RemoveArgs): Promise<void> {
+ const { project, logger, options } = args;
+ const hasWorkspaces = project.workspaceData.globs.length > 0;
+
+ logger.mainStep(
+ getMainStep({ packageManager: "npm", action: "remove", project })
+ );
+ const packageJson = getPackageJson({ workspaceRoot: project.paths.root });
+
+ if (hasWorkspaces) {
+ logger.subStep(
+ `removing "workspaces" field in ${project.name} root "package.json"`
+ );
+ delete packageJson.workspaces;
+ }
+
+ logger.subStep(
+ `removing "packageManager" field in ${project.name} root "package.json"`
+ );
+ delete packageJson.packageManager;
+
+ if (!options?.dry) {
+ fs.writeJSONSync(project.paths.packageJson, packageJson, { spaces: 2 });
+
+ // collect all workspace node_modules directories
+ const allModulesDirs = [
+ project.paths.nodeModules,
+ ...project.workspaceData.workspaces.map((w) => w.paths.nodeModules),
+ ];
+ try {
+ logger.subStep(`removing "node_modules"`);
+ await Promise.all(
+ allModulesDirs.map((dir) =>
+ fs.rm(dir, { recursive: true, force: true })
+ )
+ );
+ } catch (err) {
+ throw new ConvertError("Failed to remove node_modules", {
+ type: "error_removing_node_modules",
+ });
+ }
+ }
+}
+
+/**
+ * Clean is called post install, and is used to clean up any files
+ * from this package manager that were needed for install,
+ * but not required after migration
+ */
+async function clean(args: CleanArgs): Promise<void> {
+ const { project, logger, options } = args;
+
+ logger.subStep(
+ `removing ${path.relative(project.paths.root, project.paths.lockfile)}`
+ );
+ if (!options?.dry) {
+ fs.rmSync(project.paths.lockfile, { force: true });
+ }
+}
+
+/**
+ * Attempts to convert an existing, non npm lockfile to an npm lockfile
+ *
+ * If this is not possible, the non npm lockfile is removed
+ */
+async function convertLock(args: ConvertArgs): Promise<void> {
+ const { project, options } = args;
+
+ if (project.packageManager !== "npm") {
+ // remove the lockfile
+ if (!options?.dry) {
+ fs.rmSync(project.paths.lockfile, { force: true });
+ }
+ }
+}
+
+const npm: ManagerHandler = {
+ detect,
+ read,
+ create,
+ remove,
+ clean,
+ convertLock,
+};
+
+export default npm;
diff --git a/packages/turbo-workspaces/src/managers/pnpm.ts b/packages/turbo-workspaces/src/managers/pnpm.ts
new file mode 100644
index 0000000..747e578
--- /dev/null
+++ b/packages/turbo-workspaces/src/managers/pnpm.ts
@@ -0,0 +1,238 @@
+import fs from "fs-extra";
+import path from "path";
+import execa from "execa";
+import { ConvertError } from "../errors";
+import updateDependencies from "../updateDependencies";
+import {
+ DetectArgs,
+ ReadArgs,
+ CreateArgs,
+ RemoveArgs,
+ ConvertArgs,
+ CleanArgs,
+ Project,
+ ManagerHandler,
+} from "../types";
+import {
+ getMainStep,
+ expandPaths,
+ getWorkspaceInfo,
+ expandWorkspaces,
+ getPnpmWorkspaces,
+ getPackageJson,
+ getWorkspacePackageManager,
+} from "../utils";
+
+/**
+ * Check if a given project is using pnpm workspaces
+ * Verify by checking for the existence of:
+ * 1. pnpm-workspace.yaml
+ * 2. pnpm-workspace.yaml
+ */
+async function detect(args: DetectArgs): Promise<boolean> {
+ const lockFile = path.join(args.workspaceRoot, "pnpm-lock.yaml");
+ const workspaceFile = path.join(args.workspaceRoot, "pnpm-workspace.yaml");
+ const packageManager = getWorkspacePackageManager({
+ workspaceRoot: args.workspaceRoot,
+ });
+ return (
+ fs.existsSync(lockFile) ||
+ fs.existsSync(workspaceFile) ||
+ packageManager === "pnpm"
+ );
+}
+
+/**
+ Read workspace data from pnpm workspaces into generic format
+*/
+async function read(args: ReadArgs): Promise<Project> {
+ const isPnpm = await detect(args);
+ if (!isPnpm) {
+ throw new ConvertError("Not a pnpm project", {
+ type: "package_manager-unexpected",
+ });
+ }
+
+ const { name, description } = getWorkspaceInfo(args);
+ return {
+ name,
+ description,
+ packageManager: "pnpm",
+ paths: expandPaths({
+ root: args.workspaceRoot,
+ lockFile: "pnpm-lock.yaml",
+ workspaceConfig: "pnpm-workspace.yaml",
+ }),
+ workspaceData: {
+ globs: getPnpmWorkspaces(args),
+ workspaces: expandWorkspaces({
+ workspaceGlobs: getPnpmWorkspaces(args),
+ ...args,
+ }),
+ },
+ };
+}
+
+/**
+ * Create pnpm workspaces from generic format
+ *
+ * Creating pnpm workspaces involves:
+ * 1. Create pnpm-workspace.yaml
+ * 2. Setting the packageManager field in package.json
+ * 3. Updating all workspace package.json dependencies to ensure correct format
+ */
+async function create(args: CreateArgs): Promise<void> {
+ const { project, to, logger, options } = args;
+ const hasWorkspaces = project.workspaceData.globs.length > 0;
+
+ logger.mainStep(
+ getMainStep({ action: "create", packageManager: "pnpm", project })
+ );
+
+ const packageJson = getPackageJson({ workspaceRoot: project.paths.root });
+ logger.rootHeader();
+ packageJson.packageManager = `${to.name}@${to.version}`;
+ logger.rootStep(
+ `adding "packageManager" field to ${project.name} root "package.json"`
+ );
+
+ // write the changes
+ if (!options?.dry) {
+ fs.writeJSONSync(project.paths.packageJson, packageJson, { spaces: 2 });
+
+ if (hasWorkspaces) {
+ logger.rootStep(`adding "pnpm-workspace.yaml"`);
+ fs.writeFileSync(
+ path.join(project.paths.root, "pnpm-workspace.yaml"),
+ `packages:\n${project.workspaceData.globs
+ .map((w) => ` - "${w}"`)
+ .join("\n")}`
+ );
+ }
+ }
+
+ if (hasWorkspaces) {
+ // root dependencies
+ updateDependencies({
+ workspace: { name: "root", paths: project.paths },
+ project,
+ to,
+ logger,
+ options,
+ });
+
+ // workspace dependencies
+ logger.workspaceHeader();
+ project.workspaceData.workspaces.forEach((workspace) =>
+ updateDependencies({ workspace, project, to, logger, options })
+ );
+ }
+}
+
+/**
+ * Remove pnpm workspace data
+ *
+ * Cleaning up from pnpm involves:
+ * 1. Removing the pnpm-workspace.yaml file
+ * 2. Removing the pnpm-lock.yaml file
+ * 3. Removing the node_modules directory
+ */
+async function remove(args: RemoveArgs): Promise<void> {
+ const { project, logger, options } = args;
+ const hasWorkspaces = project.workspaceData.globs.length > 0;
+
+ logger.mainStep(
+ getMainStep({ action: "remove", packageManager: "pnpm", project })
+ );
+ const packageJson = getPackageJson({ workspaceRoot: project.paths.root });
+
+ if (project.paths.workspaceConfig && hasWorkspaces) {
+ logger.subStep(`removing "pnpm-workspace.yaml"`);
+ if (!options?.dry) {
+ fs.rmSync(project.paths.workspaceConfig, { force: true });
+ }
+ }
+
+ logger.subStep(
+ `removing "packageManager" field in ${project.name} root "package.json"`
+ );
+ delete packageJson.packageManager;
+
+ if (!options?.dry) {
+ fs.writeJSONSync(project.paths.packageJson, packageJson, { spaces: 2 });
+
+ // collect all workspace node_modules directories
+ const allModulesDirs = [
+ project.paths.nodeModules,
+ ...project.workspaceData.workspaces.map((w) => w.paths.nodeModules),
+ ];
+
+ try {
+ logger.subStep(`removing "node_modules"`);
+ await Promise.all(
+ allModulesDirs.map((dir) =>
+ fs.rm(dir, { recursive: true, force: true })
+ )
+ );
+ } catch (err) {
+ throw new ConvertError("Failed to remove node_modules", {
+ type: "error_removing_node_modules",
+ });
+ }
+ }
+}
+
+/**
+ * Clean is called post install, and is used to clean up any files
+ * from this package manager that were needed for install,
+ * but not required after migration
+ */
+async function clean(args: CleanArgs): Promise<void> {
+ const { project, logger, options } = args;
+
+ logger.subStep(
+ `removing ${path.relative(project.paths.root, project.paths.lockfile)}`
+ );
+ if (!options?.dry) {
+ fs.rmSync(project.paths.lockfile, { force: true });
+ }
+}
+
+/**
+ * Attempts to convert an existing, non pnpm lockfile to a pnpm lockfile
+ *
+ * If this is not possible, the non pnpm lockfile is removed
+ */
+async function convertLock(args: ConvertArgs): Promise<void> {
+ const { project, logger, options } = args;
+
+ if (project.packageManager !== "pnpm") {
+ logger.subStep(
+ `converting ${path.relative(
+ project.paths.root,
+ project.paths.lockfile
+ )} to pnpm-lock.yaml`
+ );
+ if (!options?.dry && fs.existsSync(project.paths.lockfile)) {
+ try {
+ await execa("pnpm", ["import"], {
+ stdio: "ignore",
+ cwd: project.paths.root,
+ });
+ } finally {
+ fs.rmSync(project.paths.lockfile, { force: true });
+ }
+ }
+ }
+}
+
+const pnpm: ManagerHandler = {
+ detect,
+ read,
+ create,
+ remove,
+ clean,
+ convertLock,
+};
+
+export default pnpm;
diff --git a/packages/turbo-workspaces/src/managers/yarn.ts b/packages/turbo-workspaces/src/managers/yarn.ts
new file mode 100644
index 0000000..9bef53f
--- /dev/null
+++ b/packages/turbo-workspaces/src/managers/yarn.ts
@@ -0,0 +1,222 @@
+import fs from "fs-extra";
+import path from "path";
+import { ConvertError } from "../errors";
+import updateDependencies from "../updateDependencies";
+import {
+ DetectArgs,
+ ReadArgs,
+ CreateArgs,
+ RemoveArgs,
+ ConvertArgs,
+ CleanArgs,
+ Project,
+} from "../types";
+import {
+ getMainStep,
+ getWorkspaceInfo,
+ getPackageJson,
+ expandPaths,
+ expandWorkspaces,
+ getWorkspacePackageManager,
+} from "../utils";
+
+/**
+ * Check if a given project is using yarn workspaces
+ * Verify by checking for the existence of:
+ * 1. yarn.lock
+ * 2. packageManager field in package.json
+ */
+async function detect(args: DetectArgs): Promise<boolean> {
+ const lockFile = path.join(args.workspaceRoot, "yarn.lock");
+ const packageManager = getWorkspacePackageManager({
+ workspaceRoot: args.workspaceRoot,
+ });
+ return fs.existsSync(lockFile) || packageManager === "yarn";
+}
+
+/**
+ Read workspace data from yarn workspaces into generic format
+*/
+async function read(args: ReadArgs): Promise<Project> {
+ const isYarn = await detect(args);
+ if (!isYarn) {
+ throw new ConvertError("Not a yarn project", {
+ type: "package_manager-unexpected",
+ });
+ }
+
+ const packageJson = getPackageJson(args);
+ const { name, description } = getWorkspaceInfo(args);
+ return {
+ name,
+ description,
+ packageManager: "yarn",
+ paths: expandPaths({
+ root: args.workspaceRoot,
+ lockFile: "yarn.lock",
+ }),
+ workspaceData: {
+ globs: packageJson.workspaces || [],
+ workspaces: expandWorkspaces({
+ workspaceGlobs: packageJson.workspaces,
+ ...args,
+ }),
+ },
+ };
+}
+
+/**
+ * Create yarn workspaces from generic format
+ *
+ * Creating yarn workspaces involves:
+ * 1. Adding the workspaces field in package.json
+ * 2. Setting the packageManager field in package.json
+ * 3. Updating all workspace package.json dependencies to ensure correct format
+ */
+async function create(args: CreateArgs): Promise<void> {
+ const { project, to, logger, options } = args;
+ const hasWorkspaces = project.workspaceData.globs.length > 0;
+
+ logger.mainStep(
+ getMainStep({ packageManager: "yarn", action: "create", project })
+ );
+ const packageJson = getPackageJson({ workspaceRoot: project.paths.root });
+ logger.rootHeader();
+
+ // package manager
+ logger.rootStep(
+ `adding "packageManager" field to ${path.relative(
+ project.paths.root,
+ project.paths.packageJson
+ )}`
+ );
+ packageJson.packageManager = `${to.name}@${to.version}`;
+
+ if (hasWorkspaces) {
+ // workspaces field
+ logger.rootStep(
+ `adding "workspaces" field to ${path.relative(
+ project.paths.root,
+ project.paths.packageJson
+ )}`
+ );
+ packageJson.workspaces = project.workspaceData.globs;
+
+ if (!options?.dry) {
+ fs.writeJSONSync(project.paths.packageJson, packageJson, { spaces: 2 });
+ }
+
+ // root dependencies
+ updateDependencies({
+ workspace: { name: "root", paths: project.paths },
+ project,
+ to,
+ logger,
+ options,
+ });
+
+ // workspace dependencies
+ logger.workspaceHeader();
+ project.workspaceData.workspaces.forEach((workspace) =>
+ updateDependencies({ workspace, project, to, logger, options })
+ );
+ } else {
+ if (!options?.dry) {
+ fs.writeJSONSync(project.paths.packageJson, packageJson, { spaces: 2 });
+ }
+ }
+}
+
+/**
+ * Remove yarn workspace data
+ *
+ * Removing yarn workspaces involves:
+ * 1. Removing the workspaces field from package.json
+ * 2. Removing the node_modules directory
+ */
+async function remove(args: RemoveArgs): Promise<void> {
+ const { project, logger, options } = args;
+ const hasWorkspaces = project.workspaceData.globs.length > 0;
+
+ logger.mainStep(
+ getMainStep({ packageManager: "yarn", action: "remove", project })
+ );
+ const packageJson = getPackageJson({ workspaceRoot: project.paths.root });
+
+ if (hasWorkspaces) {
+ logger.subStep(
+ `removing "workspaces" field in ${project.name} root "package.json"`
+ );
+ delete packageJson.workspaces;
+ }
+
+ logger.subStep(
+ `removing "packageManager" field in ${project.name} root "package.json"`
+ );
+ delete packageJson.packageManager;
+
+ if (!options?.dry) {
+ fs.writeJSONSync(project.paths.packageJson, packageJson, { spaces: 2 });
+
+ // collect all workspace node_modules directories
+ const allModulesDirs = [
+ project.paths.nodeModules,
+ ...project.workspaceData.workspaces.map((w) => w.paths.nodeModules),
+ ];
+ try {
+ logger.subStep(`removing "node_modules"`);
+ await Promise.all(
+ allModulesDirs.map((dir) =>
+ fs.rm(dir, { recursive: true, force: true })
+ )
+ );
+ } catch (err) {
+ throw new ConvertError("Failed to remove node_modules", {
+ type: "error_removing_node_modules",
+ });
+ }
+ }
+}
+
+/**
+ * Clean is called post install, and is used to clean up any files
+ * from this package manager that were needed for install,
+ * but not required after migration
+ */
+async function clean(args: CleanArgs): Promise<void> {
+ const { project, logger, options } = args;
+
+ logger.subStep(
+ `removing ${path.relative(project.paths.root, project.paths.lockfile)}`
+ );
+ if (!options?.dry) {
+ fs.rmSync(project.paths.lockfile, { force: true });
+ }
+}
+
+/**
+ * Attempts to convert an existing, non yarn lockfile to a yarn lockfile
+ *
+ * If this is not possible, the non yarn lockfile is removed
+ */
+async function convertLock(args: ConvertArgs): Promise<void> {
+ const { project, options } = args;
+
+ if (project.packageManager !== "yarn") {
+ // remove the lockfile
+ if (!options?.dry) {
+ fs.rmSync(project.paths.lockfile, { force: true });
+ }
+ }
+}
+
+const yarn = {
+ detect,
+ read,
+ create,
+ remove,
+ clean,
+ convertLock,
+};
+
+export default yarn;