|
1 | 1 | import fs from 'node:fs' |
2 | 2 | import path from 'node:path' |
3 | 3 |
|
| 4 | +import fg from 'fast-glob' |
4 | 5 | import { parse as parseToml } from 'smol-toml' |
5 | 6 |
|
6 | 7 | import { environmentVariableIsPopulated, getCustomPath, invokeCommand } from '../tools.js' |
| 8 | +import { filterManifestPathsByDiscoveryIgnore, resolveWorkspaceDiscoveryIgnore, toManifestGlobPatterns } from '../workspace.js' |
7 | 9 |
|
8 | 10 | import Base_pyproject from './base_pyproject.js' |
9 | 11 | import { evaluateMarker } from './marker_evaluator.js' |
@@ -177,3 +179,81 @@ export default class Python_uv extends Base_pyproject { |
177 | 179 | return { directDeps, graph } |
178 | 180 | } |
179 | 181 | } |
| 182 | + |
| 183 | +const DEFAULT_UV_DISCOVERY_IGNORE = [ |
| 184 | + '**/__pycache__/**', |
| 185 | + '**/.venv/**', |
| 186 | +] |
| 187 | + |
| 188 | +/** |
| 189 | + * Discover all pyproject.toml manifest paths in a uv workspace. |
| 190 | + * Parses `[tool.uv.workspace]` from root pyproject.toml and glob-expands member patterns. |
| 191 | + * |
| 192 | + * @param {string} workspaceRoot - Absolute or relative path to workspace root (must contain pyproject.toml and uv.lock) |
| 193 | + * @param {{ workspaceDiscoveryIgnore?: string[], TRUSTIFY_DA_WORKSPACE_DISCOVERY_IGNORE?: string, [key: string]: unknown }} [opts={}] |
| 194 | + * @returns {Promise<string[]>} Paths to pyproject.toml files (absolute) |
| 195 | + */ |
| 196 | +export async function discoverUvWorkspaceMembers(workspaceRoot, opts = {}) { |
| 197 | + const root = path.resolve(workspaceRoot) |
| 198 | + const rootPyproject = path.join(root, 'pyproject.toml') |
| 199 | + const uvLock = path.join(root, 'uv.lock') |
| 200 | + |
| 201 | + if (!fs.existsSync(rootPyproject) || !fs.existsSync(uvLock)) { |
| 202 | + return [] |
| 203 | + } |
| 204 | + |
| 205 | + let parsed |
| 206 | + try { |
| 207 | + parsed = parseToml(fs.readFileSync(rootPyproject, 'utf-8')) |
| 208 | + } catch { |
| 209 | + return [] |
| 210 | + } |
| 211 | + |
| 212 | + const workspaceConfig = parsed?.tool?.uv?.workspace |
| 213 | + if (!workspaceConfig) { |
| 214 | + return [] |
| 215 | + } |
| 216 | + |
| 217 | + const memberPatterns = workspaceConfig.members |
| 218 | + if (!Array.isArray(memberPatterns) || memberPatterns.length === 0) { |
| 219 | + return [] |
| 220 | + } |
| 221 | + |
| 222 | + const excludePatterns = Array.isArray(workspaceConfig.exclude) ? workspaceConfig.exclude : [] |
| 223 | + const excludeGlobs = excludePatterns |
| 224 | + .filter(p => typeof p === 'string' && p.trim()) |
| 225 | + .map(p => `${p.trim()}/pyproject.toml`) |
| 226 | + |
| 227 | + const ignorePatterns = [...resolveWorkspaceDiscoveryIgnore(opts), ...DEFAULT_UV_DISCOVERY_IGNORE] |
| 228 | + const globOpts = { |
| 229 | + cwd: root, |
| 230 | + absolute: true, |
| 231 | + onlyFiles: true, |
| 232 | + ignore: [...ignorePatterns, ...excludeGlobs], |
| 233 | + followSymbolicLinks: false, |
| 234 | + } |
| 235 | + |
| 236 | + const patterns = toManifestGlobPatterns( |
| 237 | + memberPatterns.filter(p => typeof p === 'string'), |
| 238 | + 'pyproject.toml', |
| 239 | + ) |
| 240 | + const manifestPaths = await fg(patterns, globOpts) |
| 241 | + |
| 242 | + if (!manifestPaths.includes(rootPyproject) && hasProjectMetadata(parsed)) { |
| 243 | + manifestPaths.unshift(rootPyproject) |
| 244 | + } |
| 245 | + |
| 246 | + return filterManifestPathsByDiscoveryIgnore(manifestPaths, root, ignorePatterns) |
| 247 | +} |
| 248 | + |
| 249 | +/** |
| 250 | + * @param {import('smol-toml').TomlTable} parsedPyProject |
| 251 | + * @returns {boolean} |
| 252 | + */ |
| 253 | +function hasProjectMetadata(parsedPyProject) { |
| 254 | + try { |
| 255 | + return typeof parsedPyProject?.project?.name === 'string' && parsedPyProject.project.name.trim() !== '' |
| 256 | + } catch { |
| 257 | + return false |
| 258 | + } |
| 259 | +} |
0 commit comments