diff options
Diffstat (limited to 'packages/turbo-utils/src')
| -rw-r--r-- | packages/turbo-utils/src/getTurboConfigs.ts | 106 | ||||
| -rw-r--r-- | packages/turbo-utils/src/getTurboRoot.ts | 49 | ||||
| -rw-r--r-- | packages/turbo-utils/src/index.ts | 8 | ||||
| -rw-r--r-- | packages/turbo-utils/src/managers.ts | 53 | ||||
| -rw-r--r-- | packages/turbo-utils/src/searchUp.ts | 44 |
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; |
