UNPKG

@angular/cli

Version:
247 lines • 9.32 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.LocalWorkspaceHost = exports.CommandError = void 0; exports.createRootRestrictedHost = createRootRestrictedHost; /** * @fileoverview * This file defines an abstraction layer for operating-system or file-system operations, such as * command execution. This allows for easier testing by enabling the injection of mock or * test-specific implementations. */ const fs_1 = require("fs"); const node_child_process_1 = require("node:child_process"); const node_fs_1 = require("node:fs"); const promises_1 = require("node:fs/promises"); const node_module_1 = require("node:module"); const node_net_1 = require("node:net"); const node_path_1 = require("node:path"); /** * An error thrown when a command fails to execute. */ class CommandError extends Error { logs; code; constructor(message, logs, code) { super(message); this.logs = logs; this.code = code; } } exports.CommandError = CommandError; function resolveCommand(command, args, cwd) { if (command !== 'ng' || !cwd) { return { command, args }; } try { const workspaceRequire = (0, node_module_1.createRequire)((0, node_path_1.join)(cwd, 'package.json')); const pkgJsonPath = workspaceRequire.resolve('@angular/cli/package.json'); const pkgJson = workspaceRequire(pkgJsonPath); const binPath = typeof pkgJson.bin === 'string' ? pkgJson.bin : pkgJson.bin?.['ng']; if (binPath) { const ngJsPath = (0, node_path_1.resolve)((0, node_path_1.dirname)(pkgJsonPath), binPath); return { command: process.execPath, args: [ngJsPath, ...args], }; } } catch { // Failed to resolve the CLI binary, fall back to assuming `ng` is on PATH. } return { command, args }; } /** * A concrete implementation of the `Host` interface that runs on a local workspace. */ exports.LocalWorkspaceHost = { stat: promises_1.stat, existsSync: fs_1.existsSync, readFile: promises_1.readFile, glob: function (pattern, options) { return (0, promises_1.glob)(pattern, { ...options, withFileTypes: true }); }, resolveModule(request, from) { return (0, node_module_1.createRequire)(from).resolve(request); }, runCommand: async (command, args, options = {}) => { const resolved = resolveCommand(command, args, options.cwd); const signal = options.timeout ? AbortSignal.timeout(options.timeout) : undefined; return new Promise((resolve, reject) => { const childProcess = (0, node_child_process_1.spawn)(resolved.command, resolved.args, { shell: false, stdio: options.stdio ?? 'pipe', signal, cwd: options.cwd, env: { ...process.env, ...options.env, }, }); const logs = []; childProcess.stdout?.on('data', (data) => logs.push(data.toString())); childProcess.stderr?.on('data', (data) => logs.push(data.toString())); childProcess.on('close', (code) => { if (code === 0) { resolve({ logs }); } else { const message = `Process exited with code ${code}.`; reject(new CommandError(message, logs, code)); } }); childProcess.on('error', (err) => { if (err.name === 'AbortError') { const message = `Process timed out.`; reject(new CommandError(message, logs, null)); return; } const message = `Process failed with error: ${err.message}`; reject(new CommandError(message, logs, null)); }); }); }, spawn(command, args, options = {}) { const resolved = resolveCommand(command, args, options.cwd); return (0, node_child_process_1.spawn)(resolved.command, resolved.args, { shell: false, stdio: options.stdio ?? 'pipe', cwd: options.cwd, env: { ...process.env, ...options.env, }, }); }, getAvailablePort() { return new Promise((resolve, reject) => { // Create a new temporary server from Node's net library. const server = (0, node_net_1.createServer)(); server.once('error', (err) => { reject(err); }); // Listen on port 0 to let the OS assign an available port. server.listen(0, () => { const address = server.address(); // Ensure address is an object with a port property. if (address && typeof address === 'object') { const port = address.port; server.close(); resolve(port); } else { reject(new Error('Unable to retrieve address information from server.')); } }); }); }, isPortAvailable(port) { return new Promise((resolve) => { const server = (0, node_net_1.createServer)(); server.once('error', () => resolve(false)); server.listen(port, () => { server.close(() => { resolve(true); }); }); }); }, setRoots(roots) { // LocalWorkspaceHost does not enforce roots, so this is a no-op. }, }; function createRootRestrictedHost(baseHost, initialRoots = [process.cwd()]) { let roots = initialRoots; function checkPath(path) { const resolvedPath = (0, node_path_1.resolve)(path); let realPath; try { realPath = (0, node_fs_1.realpathSync)(resolvedPath); } catch (e) { if (e.code === 'ENOENT') { // Path does not exist. Find the first existing ancestor. let current = resolvedPath; while (current) { try { realPath = (0, node_fs_1.realpathSync)(current); break; } catch (err) { if (err.code !== 'ENOENT') { throw err; } const parent = (0, node_path_1.dirname)(current); if (parent === current) { // Reached filesystem root throw err; } current = parent; } } } else { throw e; } } const isAllowed = roots.some((root) => { const rel = (0, node_path_1.relative)(root, realPath); return !rel.startsWith('..') && !(0, node_path_1.isAbsolute)(rel); }); if (!isAllowed) { throw new Error(`Access denied: path '${path}' is outside allowed roots.`); } } return { ...baseHost, setRoots(newRoots) { roots = newRoots; }, stat(path) { checkPath(path); return baseHost.stat(path); }, existsSync(path) { checkPath(path); return baseHost.existsSync(path); }, readFile(path, encoding) { checkPath(path); return baseHost.readFile(path, encoding); }, glob(pattern, options) { if (pattern.includes('..')) { throw new Error(`Access denied: glob pattern '${pattern}' contains path traversal sequences.`); } checkPath(options.cwd); const firstWildcardIndex = pattern.search(/[*?[{]/); const basePath = firstWildcardIndex >= 0 ? pattern.substring(0, firstWildcardIndex) : pattern; const targetDir = (0, node_path_1.resolve)(options.cwd, basePath); checkPath(targetDir); return baseHost.glob(pattern, options); }, runCommand(command, args, options = {}) { const effectiveCwd = options.cwd ?? process.cwd(); checkPath(effectiveCwd); if (command.includes('/') || command.includes('\\')) { checkPath((0, node_path_1.resolve)(effectiveCwd, command)); } return baseHost.runCommand(command, args, options); }, spawn(command, args, options = {}) { const effectiveCwd = options.cwd ?? process.cwd(); checkPath(effectiveCwd); if (command.includes('/') || command.includes('\\')) { checkPath((0, node_path_1.resolve)(effectiveCwd, command)); } return baseHost.spawn(command, args, options); }, }; } //# sourceMappingURL=host.js.map