From dd84b9d64fb98746a230cd24233ff50a562c39c9 Mon Sep 17 00:00:00 2001 From: 简律纯 Date: Fri, 28 Apr 2023 01:36:44 +0800 Subject: --- packages/turbo-workspaces/src/managers/index.ts | 11 ++ packages/turbo-workspaces/src/managers/npm.ts | 223 ++++++++++++++++++++++ packages/turbo-workspaces/src/managers/pnpm.ts | 238 ++++++++++++++++++++++++ packages/turbo-workspaces/src/managers/yarn.ts | 222 ++++++++++++++++++++++ 4 files changed, 694 insertions(+) create mode 100644 packages/turbo-workspaces/src/managers/index.ts create mode 100644 packages/turbo-workspaces/src/managers/npm.ts create mode 100644 packages/turbo-workspaces/src/managers/pnpm.ts create mode 100644 packages/turbo-workspaces/src/managers/yarn.ts (limited to 'packages/turbo-workspaces/src/managers') 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 = { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; -- cgit v1.2.3-70-g09d2