Created
February 2, 2024 11:57
-
-
Save Danielduel/b4f945960d8f3c7d61a1b5beebfab208 to your computer and use it in GitHub Desktop.
Deno cross-link dependencies of local projects using yarn.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Cross-link dependencies of local projects using yarn. | |
* The main use-case of this script is when you have 2 or more projects set up locally, | |
* first is a monorepo containing dependencies to the 2nd project which consumes them. | |
* | |
* Required dependencies: | |
* You need to have `find`, `sh` and `which` installed. | |
* `which` has to see `yarn` from within target project's path. | |
* All dependencies has to be visible to spawned `/bin/sh` so most likely this script | |
* can't be sandboxed. | |
* | |
* Required arguments: | |
* --source_path <path[,path,...]> Source path - one or more (comma separated) paths of projects | |
* your dependencies. | |
* --target_path <path> Target path - one path to the dependency consumer. | |
* | |
* Optional arguments: | |
* --filters <scope[,scope,...]> Filters - one or more (comma separated) filters which behave | |
* as startsWith of packages to take into consideration of linking | |
* | |
* Arguments modifying the operation: | |
* --link Links all items. (default) | |
* --unlink_before_linking Unlinks and unregisters links before registering and linking | |
* them again. | |
* --unlink Unlinks links. | |
* --unlink_and_remove_links Unlinks links and unregisters linking of sources. | |
* | |
* Example usage: | |
* deno run -A LINK_TO_RAW_OF_THIS_GIST \ | |
* --unlink_before_linking \ | |
* --source_path "/path/to/your/dependency1,/path/to/your/dependency2" \ | |
* --target_path "/path/to/app" \ | |
* --filters "@scope1,@scope2" | |
*/ | |
import { parseArgs } from "https://deno.land/[email protected]/cli/parse_args.ts"; | |
const args = parseArgs(Deno.args, { | |
string: ["source_path", "target_path", "filters"], | |
boolean: [ | |
"link", | |
"unlink_before_linking", | |
"unlink", | |
"unlink_and_remove_links", | |
], | |
}); | |
const requiredArg = (argument: unknown, argumentName: string) => { | |
if (!argument) { | |
console.log(`${argumentName} is required`); | |
Deno.exit(); | |
} | |
}; | |
const link = args.link; | |
const unlink = args.unlink; | |
const unlinkBeforeLinking = args.unlink_before_linking; | |
const unlinkAndRemoveLinks = args.unlink_and_remove_links; | |
const sourcePathAsString = args.source_path!; | |
requiredArg(sourcePathAsString, "source_path"); | |
const targetPath = args.target_path!; | |
requiredArg(targetPath, "target_path"); | |
const filtersAsString = args.filters!; | |
requiredArg(filtersAsString, "filters"); | |
const executeOrReturn = <T>(returnable: T) => { | |
if (typeof returnable === "function") { | |
return returnable(); | |
} | |
return returnable; | |
}; | |
const _match = | |
( | |
linkArg: boolean, | |
unlinkArg: boolean, | |
unlinkBeforeLinkingArg: boolean, | |
unlinkAndRemoveLinksArg: boolean | |
) => | |
<T>({ | |
link, | |
unlink, | |
unlinkBeforeLinking, | |
unlinkAndRemoveLinks, | |
}: { | |
link: T; | |
unlink: T; | |
unlinkBeforeLinking: T; | |
unlinkAndRemoveLinks: T; | |
}): T => { | |
if (unlinkAndRemoveLinksArg) { | |
return executeOrReturn(unlinkAndRemoveLinks); | |
} | |
if (unlinkBeforeLinkingArg) { | |
return executeOrReturn(unlinkBeforeLinking); | |
} | |
if (unlinkArg) { | |
return executeOrReturn(unlink); | |
} | |
// linkArg is ignored | |
return executeOrReturn(link); | |
}; | |
const match = _match(link, unlink, unlinkBeforeLinking, unlinkAndRemoveLinks); | |
const filters = filtersAsString.split(",").filter((x) => !!x); | |
console.log("Parsed filter array: " + JSON.stringify(filters)); | |
const sourcePaths = sourcePathAsString.split(",").filter((x) => !!x); | |
const packageContent: Record<string, Record<string, unknown>> = JSON.parse( | |
await Deno.readTextFile(`${targetPath}/package.json`) | |
); | |
const getProjectYarn = async (projectPath: string) => { | |
const command = new Deno.Command("/bin/sh", { | |
cwd: projectPath, | |
args: ["-c", "which yarn"], | |
}); | |
const { stdout /* stderr */ } = await command.output(); | |
const stdoutParsed = new TextDecoder().decode(stdout); | |
return stdoutParsed.replaceAll("\n", ""); | |
}; | |
const yarnPathP = getProjectYarn(targetPath); | |
const discoverSourcePackages = async (sourcePath: string) => { | |
console.log(`Discovery started for ${sourcePath}`); | |
const command = new Deno.Command("/bin/sh", { | |
cwd: sourcePath, | |
args: [ | |
"-c", | |
"find . -name package.json -maxdepth 7 ! -path '*/node_modules/*'", | |
], | |
}); | |
const { stdout /* stderr */ } = await command.output(); | |
const stdoutParsed = new TextDecoder().decode(stdout); | |
const packagePaths = stdoutParsed.split("\n").filter((x) => !!x); | |
console.log( | |
`Discovery find step done for ${sourcePath} (${packagePaths.length} items)` | |
); | |
return packagePaths.reduce((p, c) => { | |
const fullPath = new URL(import.meta.resolve(sourcePath + "/" + c)) | |
.pathname; | |
const fullPathFolder = fullPath.split("/package.json")[0]; | |
const { name } = JSON.parse(Deno.readTextFileSync(fullPath)); | |
return { | |
...p, | |
[name]: fullPathFolder, | |
}; | |
}, {} as Record<string, string>); | |
}; | |
const availablePackagesP = sourcePaths.map(discoverSourcePackages); | |
const packagesInTargetProject = [ | |
...Object.keys(packageContent.dependencies), | |
...Object.keys(packageContent.devDependencies), | |
]; | |
const packagesToProcess = packagesInTargetProject.filter((packageName) => | |
filters.some((filter) => packageName.startsWith(filter)) | |
); | |
const processSourcePath = async (linkingSourcePath: string) => { | |
const yarnPath = await yarnPathP; | |
const shellString = match({ | |
unlink: ``, | |
unlinkAndRemoveLinks: `${yarnPath} unlink`, | |
unlinkBeforeLinking: `${yarnPath} unlink; ${yarnPath} link`, | |
link: `${yarnPath} link`, | |
}); | |
const command = new Deno.Command("/bin/sh", { | |
cwd: linkingSourcePath, | |
args: ["-c", shellString], | |
}); | |
const { stdout, stderr } = await command.output(); | |
const stdoutParsed = new TextDecoder().decode(stdout); | |
const stderrParsed = new TextDecoder().decode(stderr); | |
console.log(stdoutParsed); | |
console.log(stderrParsed); | |
return stdoutParsed; | |
}; | |
const processTargetPath = async ( | |
targetProjectPath: string, | |
packageToLink: string | |
) => { | |
const yarnPath = await yarnPathP; | |
const shellString = match({ | |
unlink: `${yarnPath} unlink ${packageToLink}`, | |
unlinkAndRemoveLinks: `${yarnPath} unlink ${packageToLink};`, | |
unlinkBeforeLinking: `${yarnPath} unlink ${packageToLink}; ${yarnPath} link ${packageToLink}`, | |
link: `${yarnPath} link ${packageToLink}`, | |
}); | |
const command = new Deno.Command("/bin/sh", { | |
cwd: targetProjectPath, | |
args: ["-c", shellString], | |
}); | |
const { stdout, stderr } = await command.output(); | |
const stdoutParsed = new TextDecoder().decode(stdout); | |
const stderrParsed = new TextDecoder().decode(stderr); | |
console.log(stdoutParsed); | |
console.log(stderrParsed); | |
return stdoutParsed; | |
}; | |
const processPackage = async ( | |
targetProjectPath: string, | |
packageToLink: string, | |
sourcePackages: Record<string, string> | |
) => { | |
if (!(packageToLink in sourcePackages)) return null; | |
const linkingSourcePath = sourcePackages[packageToLink]; | |
await processSourcePath(linkingSourcePath); | |
await processTargetPath(targetProjectPath, packageToLink); | |
}; | |
const processPackages = ( | |
targetProjectPath: string, | |
packagesToProcess: string[], | |
sourcePackages: Record<string, string> | |
) => { | |
return packagesToProcess.map((packageName) => | |
processPackage(targetProjectPath, packageName, sourcePackages) | |
); | |
}; | |
await Promise.all( | |
processPackages( | |
targetPath, | |
packagesToProcess, | |
(await Promise.all(availablePackagesP)).reduce((p, c) => ({ ...p, ...c })) | |
) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment