aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/packages/turbo-utils/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/turbo-utils/src')
-rw-r--r--packages/turbo-utils/src/getTurboConfigs.ts106
-rw-r--r--packages/turbo-utils/src/getTurboRoot.ts49
-rw-r--r--packages/turbo-utils/src/index.ts8
-rw-r--r--packages/turbo-utils/src/managers.ts53
-rw-r--r--packages/turbo-utils/src/searchUp.ts44
5 files changed, 260 insertions, 0 deletions
diff --git a/packages/turbo-utils/src/getTurboConfigs.ts b/packages/turbo-utils/src/getTurboConfigs.ts
new file mode 100644
index 0000000..df15a56
--- /dev/null
+++ b/packages/turbo-utils/src/getTurboConfigs.ts
@@ -0,0 +1,106 @@
+import fs from "fs";
+import path from "path";
+import getTurboRoot from "./getTurboRoot";
+import yaml from "js-yaml";
+import { sync } from "fast-glob";
+import { Schema } from "@turbo/types";
+import JSON5 from "json5";
+
+const ROOT_GLOB = "turbo.json";
+
+export type TurboConfigs = Array<{
+ config: Schema;
+ turboConfigPath: string;
+ workspacePath: string;
+ isRootConfig: boolean;
+}>;
+
+interface Options {
+ cache?: boolean;
+}
+
+const configsCache: Record<string, TurboConfigs> = {};
+
+// A quick and dirty workspace parser
+// TODO: after @turbo/workspace-convert is merged, we can leverage those utils here
+function getWorkspaceGlobs(root: string): Array<string> {
+ try {
+ if (fs.existsSync(path.join(root, "pnpm-workspace.yaml"))) {
+ const workspaceConfig = yaml.load(
+ fs.readFileSync(path.join(root, "pnpm-workspace.yaml"), "utf8")
+ ) as Record<"packages", Array<string>>;
+
+ return workspaceConfig?.packages || [];
+ } else {
+ const packageJson = JSON.parse(
+ fs.readFileSync(path.join(root, "package.json"), "utf8")
+ );
+ return packageJson?.workspaces || [];
+ }
+ } catch (e) {
+ return [];
+ }
+}
+
+function getTurboConfigs(cwd?: string, opts?: Options): TurboConfigs {
+ const turboRoot = getTurboRoot(cwd, opts);
+ const configs: TurboConfigs = [];
+
+ const cacheEnabled = opts?.cache ?? true;
+ if (cacheEnabled && cwd && configsCache[cwd]) {
+ return configsCache[cwd];
+ }
+
+ // parse workspaces
+ if (turboRoot) {
+ const workspaceGlobs = getWorkspaceGlobs(turboRoot);
+ const workspaceConfigGlobs = workspaceGlobs.map(
+ (glob) => `${glob}/turbo.json`
+ );
+
+ const configPaths = sync([ROOT_GLOB, ...workspaceConfigGlobs], {
+ cwd: turboRoot,
+ onlyFiles: true,
+ followSymbolicLinks: false,
+ // avoid throwing when encountering permission errors or unreadable paths
+ suppressErrors: true,
+ }).map((configPath) => path.join(turboRoot, configPath));
+
+ configPaths.forEach((configPath) => {
+ try {
+ const raw = fs.readFileSync(configPath, "utf8");
+ const turboJsonContent: Schema = JSON5.parse(raw);
+ // basic config validation
+ let isRootConfig = path.dirname(configPath) === turboRoot;
+ if (isRootConfig) {
+ // invalid - root config with extends
+ if ("extends" in turboJsonContent) {
+ return;
+ }
+ } else {
+ // invalid - workspace config with no extends
+ if (!("extends" in turboJsonContent)) {
+ return;
+ }
+ }
+ configs.push({
+ config: turboJsonContent,
+ turboConfigPath: configPath,
+ workspacePath: path.dirname(configPath),
+ isRootConfig,
+ });
+ } catch (e) {
+ // if we can't read or parse the config, just ignore it with a warning
+ console.warn(e);
+ }
+ });
+ }
+
+ if (cacheEnabled && cwd) {
+ configsCache[cwd] = configs;
+ }
+
+ return configs;
+}
+
+export default getTurboConfigs;
diff --git a/packages/turbo-utils/src/getTurboRoot.ts b/packages/turbo-utils/src/getTurboRoot.ts
new file mode 100644
index 0000000..64a37be
--- /dev/null
+++ b/packages/turbo-utils/src/getTurboRoot.ts
@@ -0,0 +1,49 @@
+import { findRootSync } from "@manypkg/find-root";
+import searchUp from "./searchUp";
+import JSON5 from "json5";
+
+interface Options {
+ cache?: boolean;
+}
+
+function contentCheck(content: string): boolean {
+ const result = JSON5.parse(content);
+ return !result.extends;
+}
+
+const configCache: Record<string, string> = {};
+
+function getTurboRoot(cwd?: string, opts?: Options): string | null {
+ const cacheEnabled = opts?.cache ?? true;
+ const currentDir = cwd || process.cwd();
+
+ if (cacheEnabled && configCache[currentDir]) {
+ return configCache[currentDir];
+ }
+
+ // Turborepo root can be determined by a turbo.json without an extends key
+ let root = searchUp({
+ target: "turbo.json",
+ cwd: currentDir,
+ contentCheck,
+ });
+
+ if (!root) {
+ try {
+ root = findRootSync(currentDir);
+ if (!root) {
+ return null;
+ }
+ } catch (err) {
+ return null;
+ }
+ }
+
+ if (cacheEnabled) {
+ configCache[currentDir] = root;
+ }
+
+ return root;
+}
+
+export default getTurboRoot;
diff --git a/packages/turbo-utils/src/index.ts b/packages/turbo-utils/src/index.ts
new file mode 100644
index 0000000..2d86559
--- /dev/null
+++ b/packages/turbo-utils/src/index.ts
@@ -0,0 +1,8 @@
+// utils
+export { default as getTurboRoot } from "./getTurboRoot";
+export { default as getTurboConfigs } from "./getTurboConfigs";
+export { default as searchUp } from "./searchUp";
+export { getAvailablePackageManagers } from "./managers";
+
+// types
+export type { PackageManagerAvailable } from "./managers";
diff --git a/packages/turbo-utils/src/managers.ts b/packages/turbo-utils/src/managers.ts
new file mode 100644
index 0000000..ab9c53d
--- /dev/null
+++ b/packages/turbo-utils/src/managers.ts
@@ -0,0 +1,53 @@
+import execa from "execa";
+import os from "os";
+
+export type PackageManager = "npm" | "yarn" | "pnpm";
+export type PackageManagerAvailable = { available: boolean; version?: string };
+
+async function getVersion(
+ packageManager: string
+): Promise<PackageManagerAvailable> {
+ // run the check from tmpdir to avoid corepack conflicting -
+ // this is no longer needed as of https://github.com/nodejs/corepack/pull/167
+ // but we'll keep the behavior for those on older versions)
+ const execOptions = {
+ cwd: os.tmpdir(),
+ env: { COREPACK_ENABLE_STRICT: "0" },
+ };
+
+ let available = false;
+ try {
+ const userAgent = process.env.npm_config_user_agent;
+ if (userAgent && userAgent.startsWith(packageManager)) {
+ available = true;
+ }
+
+ const result = await execa(packageManager, ["--version"], execOptions);
+ return {
+ available: true,
+ version: result.stdout.trim(),
+ };
+ } catch (e) {
+ return {
+ available,
+ };
+ }
+}
+
+async function getAvailablePackageManagers(): Promise<
+ Record<PackageManager, PackageManagerAvailable>
+> {
+ const [yarn, npm, pnpm] = await Promise.all([
+ getVersion("yarnpkg"),
+ getVersion("npm"),
+ getVersion("pnpm"),
+ ]);
+
+ return {
+ yarn,
+ pnpm,
+ npm,
+ };
+}
+
+export { getAvailablePackageManagers };
diff --git a/packages/turbo-utils/src/searchUp.ts b/packages/turbo-utils/src/searchUp.ts
new file mode 100644
index 0000000..57f92e4
--- /dev/null
+++ b/packages/turbo-utils/src/searchUp.ts
@@ -0,0 +1,44 @@
+import fs from "fs";
+import path from "path";
+
+function searchUp({
+ target,
+ cwd,
+ contentCheck,
+}: {
+ target: string;
+ cwd: string;
+ contentCheck?: (content: string) => boolean;
+}): string | null {
+ const root = path.parse(cwd).root;
+
+ let found = false;
+ while (!found && cwd !== root) {
+ if (contentCheck) {
+ try {
+ const content = fs.readFileSync(path.join(cwd, target)).toString();
+ if (contentCheck(content)) {
+ found = true;
+ break;
+ }
+ } catch {
+ // keep looking
+ }
+ } else {
+ if (fs.existsSync(path.join(cwd, target))) {
+ found = true;
+ break;
+ }
+ }
+
+ cwd = path.dirname(cwd);
+ }
+
+ if (found) {
+ return cwd;
+ }
+
+ return null;
+}
+
+export default searchUp;