UNPKG

@angular/cli

Version:
544 lines 21.1 kB
"use strict"; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ Object.defineProperty(exports, "__esModule", { value: true }); exports.parseNpmLikeDependencies = parseNpmLikeDependencies; exports.parseYarnClassicDependencies = parseYarnClassicDependencies; exports.parseNpmLikeManifest = parseNpmLikeManifest; exports.parseNpmLikeMetadata = parseNpmLikeMetadata; exports.parseYarnClassicManifest = parseYarnClassicManifest; exports.parseYarnClassicMetadata = parseYarnClassicMetadata; exports.parseNpmLikeError = parseNpmLikeError; exports.parseYarnClassicError = parseYarnClassicError; exports.parseBunDependencies = parseBunDependencies; exports.parseYarnModernDependencies = parseYarnModernDependencies; /** * @fileoverview This file contains the parser functions that are used to * interpret the output of various package manager commands. Separating these * into their own file improves modularity and allows for focused testing. */ const semver_1 = require("semver"); const MAX_LOG_LENGTH = 1024; function logStdout(stdout, logger) { if (!logger) { return; } let output = stdout; if (output.length > MAX_LOG_LENGTH) { output = `${output.slice(0, MAX_LOG_LENGTH)}... (truncated)`; } logger.debug(` stdout:\n${output}`); } /** * A generator function that parses a string containing JSONL (newline-delimited JSON) * and yields each successfully parsed JSON object. * @param output The string output to parse. * @param logger An optional logger instance. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function* parseJsonLines(output, logger) { for (const line of output.split('\n')) { if (!line.trim()) { continue; } try { yield JSON.parse(line); } catch (e) { logger?.debug(` Ignoring non-JSON line: ${e}`); } } } /** * Parses the output of `npm list` or a compatible command. * * The expected JSON structure is: * ```json * { * "dependencies": { * "@angular/cli": { * "version": "18.0.0", * "path": "/path/to/project/node_modules/@angular/cli", // path is optional * ... (other package.json properties) * } * } * } * ``` * * @param stdout The standard output of the command. * @param logger An optional logger instance. * @returns A map of package names to their installed package details. */ function parseNpmLikeDependencies(stdout, logger, options) { logger?.debug(`Parsing npm-like dependency list...`); logStdout(stdout, logger); const dependencies = new Map(); if (!stdout) { logger?.debug(' stdout is empty. No dependencies found.'); return dependencies; } let data = JSON.parse(stdout); if (Array.isArray(data)) { // pnpm returns an array of projects. data = data[0]; } const dependencyMaps = [data.dependencies, data.devDependencies, data.unsavedDependencies].filter((d) => !!d); if (dependencyMaps.length === 0) { logger?.debug(' `dependencies` property not found. No dependencies found.'); return dependencies; } const workspacePackageName = options?.workspacePackageName; if (workspacePackageName) { for (const dependencyMap of dependencyMaps) { const info = dependencyMap[workspacePackageName]; if (info && typeof info === 'object') { const nestedMaps = [ info.dependencies, info.devDependencies, info.unsavedDependencies, ].filter((d) => !!d); for (const nestedMap of nestedMaps) { for (const [name, nestedInfo] of Object.entries(nestedMap)) { if (nestedInfo && typeof nestedInfo === 'object' && nestedInfo.version) { dependencies.set(name, { name, version: nestedInfo.version, path: nestedInfo.path, }); } } } } } } // Extract top-level dependencies (root), without overwriting subproject dependencies for (const dependencyMap of dependencyMaps) { for (const [name, info] of Object.entries(dependencyMap)) { if (!info || typeof info !== 'object') { continue; } // Exclude local monorepo workspace packages (which originate from a local file/dir // and contain nested dependency maps in the output of `npm list --depth=0`), // while preserving third-party packages installed from local paths. const isWorkspacePackage = info.resolved?.startsWith('file:') && (!!info.dependencies || !!info.devDependencies || !!info.unsavedDependencies); if (info.version && !dependencies.has(name) && !isWorkspacePackage) { dependencies.set(name, { name, version: info.version, path: info.path, }); } } } logger?.debug(` Found ${dependencies.size} dependencies.`); return dependencies; } /** * Parses the output of `yarn list` (classic). * * The expected output is a JSON stream (JSONL), where each line is a JSON object. * The relevant object has a `type` of `'tree'` with a `data` property. * Yarn classic does not provide a path, so the `path` property will be `undefined`. * * ```json * {"type":"tree","data":{"trees":[{"name":"@angular/cli@18.0.0","children":[]}]}} * ``` * * @param stdout The standard output of the command. * @param logger An optional logger instance. * @returns A map of package names to their installed package details. */ function parseYarnClassicDependencies(stdout, logger) { logger?.debug(`Parsing yarn classic dependency list...`); logStdout(stdout, logger); const dependencies = new Map(); if (!stdout) { logger?.debug(' stdout is empty. No dependencies found.'); return dependencies; } for (const json of parseJsonLines(stdout, logger)) { if (json.type === 'tree' && json.data?.trees) { for (const info of json.data.trees) { const lastAtIndex = info.name.lastIndexOf('@'); const name = info.name.slice(0, lastAtIndex); const version = info.name.slice(lastAtIndex + 1); dependencies.set(name, { name, version, }); } } } logger?.debug(` Found ${dependencies.size} dependencies.`); return dependencies; } function isValidManifest(obj) { if (typeof obj !== 'object' || obj === null) { return false; } const record = obj; const name = record.name; const version = record.version; return typeof name === 'string' && typeof version === 'string' && (0, semver_1.valid)(version) !== null; } /** * Parses the output of `npm view` or a compatible command to get a package manifest. * @param stdout The standard output of the command. * @param logger An optional logger instance. * @returns The package manifest object. */ function parseNpmLikeManifest(stdout, logger) { logger?.debug(`Parsing npm-like manifest...`); logStdout(stdout, logger); if (!stdout) { logger?.debug(' stdout is empty. No manifest found.'); return null; } const result = JSON.parse(stdout); // npm view returns an array of manifests if the query matches multiple versions // (e.g. when using a version range). We find the highest version to ensure // we get the latest relevant manifest, even if the output is not sorted. if (Array.isArray(result)) { let maxManifest = null; for (const manifest of result) { if (!isValidManifest(manifest)) { logger?.debug(' Skipping invalid manifest in array (missing name, version, or invalid SemVer).'); continue; } if (!maxManifest || (0, semver_1.compare)(manifest.version, maxManifest.version) > 0) { maxManifest = manifest; } } if (!maxManifest) { logger?.debug(' No valid manifests found in the array.'); } return maxManifest; } if (!isValidManifest(result)) { logger?.debug(' Parsed JSON is not a valid manifest (missing name, version, or invalid SemVer).'); return null; } return result; } /** * Parses the output of `npm view` or a compatible command to get package metadata. * @param stdout The standard output of the command. * @param logger An optional logger instance. * @returns The package metadata object. */ function parseNpmLikeMetadata(stdout, logger) { logger?.debug(`Parsing npm-like metadata...`); logStdout(stdout, logger); if (!stdout) { logger?.debug(' stdout is empty. No metadata found.'); return null; } return JSON.parse(stdout); } /** * Parses the output of `yarn info` (classic) to get a package manifest. * * When `yarn info --verbose` is used, the output is a JSONL stream. This function * iterates through the lines to find the object with `type: 'inspect'` which contains * the package manifest. * * For non-verbose output, it falls back to parsing a single JSON object. * * @param stdout The standard output of the command. * @param logger An optional logger instance. * @returns The package manifest object, or `null` if not found. */ function parseYarnClassicManifest(stdout, logger) { logger?.debug(`Parsing yarn classic manifest...`); logStdout(stdout, logger); if (!stdout) { logger?.debug(' stdout is empty. No manifest found.'); return null; } // Yarn classic outputs JSONL. We need to find the relevant object. let manifest; for (const json of parseJsonLines(stdout, logger)) { // The manifest data is in a JSON object with type 'inspect'. if (json.type === 'inspect' && json.data) { manifest = json.data; break; } } if (!manifest) { logger?.debug(' Failed to find manifest in yarn classic output.'); return null; } // Yarn classic removes any field with a falsy value // https://github.com/yarnpkg/yarn/blob/7cafa512a777048ce0b666080a24e80aae3d66a9/src/cli/commands/info.js#L26-L29 // Add a default of 'false' for the `save` field when the `ng-add` object is present but does not have any fields. // There is a small chance this causes an incorrect value. However, the use of `ng-add` is rare and, in the cases // it is used, save is set to either a `false` literal or a truthy value. Special cases can be added for specific // packages if discovered. if (manifest['ng-add'] && typeof manifest['ng-add'] === 'object' && Object.keys(manifest['ng-add']).length === 0) { manifest['ng-add'].save ??= false; } if (!isValidManifest(manifest)) { logger?.debug(' Parsed JSON is not a valid manifest (missing name, version, or invalid SemVer).'); return null; } return manifest; } /** * Parses the output of `yarn info` (classic) to get package metadata. * @param stdout The standard output of the command. * @param logger An optional logger instance. * @returns The package metadata object. */ function parseYarnClassicMetadata(stdout, logger) { logger?.debug(`Parsing yarn classic metadata...`); logStdout(stdout, logger); if (!stdout) { logger?.debug(' stdout is empty. No metadata found.'); return null; } // Yarn classic outputs JSONL. We need to find the relevant object. let metadata; for (const json of parseJsonLines(stdout, logger)) { // The metadata data is in a JSON object with type 'inspect'. if (json.type === 'inspect' && json.data) { metadata = json.data; break; } } if (!metadata) { logger?.debug(' Failed to find metadata in yarn classic output.'); return null; } return metadata; } /** * Parses the `stdout` or `stderr` output of npm, pnpm, modern yarn, or bun to extract structured error information. * * This parser uses a multi-stage approach. It first attempts to parse the entire `output` as a * single JSON object, which is the standard for modern tools like pnpm, yarn, and bun. If JSON * parsing fails, it falls back to a line-by-line regex-based approach to handle the plain * text output from older versions of npm. * * Example JSON output (pnpm): * ```json * { * "code": "E404", * "summary": "Not Found - GET https://registry.npmjs.org/@angular%2fnon-existent - Not found", * "detail": "The requested resource '@angular/non-existent@*' could not be found or you do not have permission to access it." * } * ``` * * Example text output (npm): * ``` * npm error code E404 * npm error 404 Not Found - GET https://registry.npmjs.org/@angular%2fnon-existent - Not found * ``` * * @param output The standard output or standard error of the command. * @param logger An optional logger instance. * @returns An `ErrorInfo` object if parsing is successful, otherwise `null`. */ function parseNpmLikeError(output, logger) { logger?.debug(`Parsing npm-like error output...`); logStdout(output, logger); // Log output for debugging purposes if (!output) { logger?.debug(' output is empty. No error found.'); return null; } // Attempt to parse as JSON first (common for pnpm, modern yarn, bun) try { const jsonError = JSON.parse(output); if (jsonError && typeof jsonError.code === 'string' && (typeof jsonError.summary === 'string' || typeof jsonError.message === 'string')) { const summary = jsonError.summary || jsonError.message; logger?.debug(` Successfully parsed JSON error with code '${jsonError.code}'.`); return { code: jsonError.code, summary, detail: jsonError.detail, }; } } catch (e) { logger?.debug(` Failed to parse output as JSON: ${e}. Attempting regex fallback.`); // Fallback to regex for plain text errors (common for npm) } // Regex for npm-like error codes (e.g., `npm ERR! code E404` or `npm error code E404`) const errorCodeMatch = output.match(/npm (ERR!|error) code (E\d{3}|[A-Z_]+)/); if (errorCodeMatch) { const code = errorCodeMatch[2]; // Capture group 2 is the actual error code let summary; // Find the most descriptive summary line (the line after `npm ERR! code ...` or `npm error code ...`). for (const line of output.split('\n')) { if (line.startsWith('npm ERR!') && !line.includes(' code ')) { summary = line.replace('npm ERR! ', '').trim(); break; } else if (line.startsWith('npm error') && !line.includes(' code ')) { summary = line.replace('npm error ', '').trim(); break; } } logger?.debug(` Successfully parsed text error with code '${code}'.`); return { code, summary: summary || `Package manager error: ${code}`, }; } logger?.debug(' Failed to parse npm-like error. No structured error found.'); return null; } /** * Parses the `stdout` or `stderr` output of yarn classic to extract structured error information. * * This parser first attempts to find an HTTP status code (e.g., 404, 401) in the verbose output. * If found, it returns a standardized error code (`E${statusCode}`). * If no HTTP status code is found, it falls back to parsing generic JSON error lines. * * Example verbose output (with HTTP status code): * ```json * {"type":"verbose","data":"Request \"https://registry.npmjs.org/@angular%2fnon-existent\" finished with status code 404."} * ``` * * Example generic JSON error output: * ```json * {"type":"error","data":"Received invalid response from npm."} * ``` * * @param output The standard output or standard error of the command. * @param logger An optional logger instance. * @returns An `ErrorInfo` object if parsing is successful, otherwise `null`. */ function parseYarnClassicError(output, logger) { logger?.debug(`Parsing yarn classic error output...`); logStdout(output, logger); // Log output for debugging purposes if (!output) { logger?.debug(' output is empty. No error found.'); return null; } // First, check for any HTTP status code in the verbose output. const statusCodeMatch = output.match(/finished with status code (\d{3})/); if (statusCodeMatch) { const statusCode = Number(statusCodeMatch[1]); // Status codes in the 200-299 range are successful. if (statusCode < 200 || statusCode >= 300) { logger?.debug(` Detected HTTP error status code '${statusCode}' in verbose output.`); return { code: `E${statusCode}`, summary: `Request failed with status code ${statusCode}.`, }; } } // Fallback to the JSON error type if no HTTP status code is present. for (const json of parseJsonLines(output, logger)) { if (json.type === 'error' && typeof json.data === 'string') { const summary = json.data; logger?.debug(` Successfully parsed generic yarn classic error.`); return { code: 'UNKNOWN_ERROR', summary, }; } } logger?.debug(' Failed to parse yarn classic error. No structured error found.'); return null; } /** * Parses the output of `bun pm ls`. * * Bun does not support JSON output for `pm ls`. The output is a tree structure: * ``` * /path/to/project node_modules (1084) * ├── @angular/core@20.3.15 * ├── rxjs @7.8.2 * └── zone.js @0.15.1 * ``` * * @param stdout The standard output of the command. * @param logger An optional logger instance. * @returns A map of package names to their installed package details. */ function parseBunDependencies(stdout, logger) { logger?.debug('Parsing Bun dependency list...'); logStdout(stdout, logger); const dependencies = new Map(); if (!stdout) { return dependencies; } const lines = stdout.split('\n'); // Skip the first line (project info) for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) { continue; } // Remove tree structure characters const cleanLine = line.replace(/^[└├]──\s*/, ''); // Parse name and version // Scoped: @angular/core@20.3.15 // Unscoped: rxjs @7.8.2 const match = cleanLine.match(/^(.+?)\s?@([^@\s]+)$/); if (match) { const name = match[1]; const version = match[2]; dependencies.set(name, { name, version }); } } logger?.debug(` Found ${dependencies.size} dependencies.`); return dependencies; } /** * Parses the output of `yarn info --name-only --json`. * * The expected output is a JSON stream (JSONL) of strings. * Each string represents a package locator. * * ``` * "karma@npm:6.4.4" * "@angular/core@npm:20.3.15" * ``` * * @param stdout The standard output of the command. * @param logger An optional logger instance. * @returns A map of package names to their installed package details. */ function parseYarnModernDependencies(stdout, logger) { logger?.debug('Parsing Yarn Berry dependency list...'); logStdout(stdout, logger); const dependencies = new Map(); if (!stdout) { return dependencies; } for (const json of parseJsonLines(stdout, logger)) { if (typeof json === 'string') { const match = json.match(/^(@?[^@]+)@(.+)$/); if (match) { const name = match[1]; let version = match[2]; // Handle "npm:" prefix if (version.startsWith('npm:')) { version = version.slice(4); } // Handle complex locators with embedded version metadata (e.g., "patch:...", "virtual:...") // Yarn Berry often appends metadata like "::version=x.y.z" const versionParamMatch = version.match(/::version=([^&]+)/); if (versionParamMatch) { version = versionParamMatch[1]; } dependencies.set(name, { name, version }); } } } logger?.debug(` Found ${dependencies.size} dependencies.`); return dependencies; } //# sourceMappingURL=parsers.js.map