UNPKG

@netlify/build-info

Version:
307 lines 12.3 kB
import { coerce, parse } from 'semver'; import { buildSystems } from './build-systems/index.js'; import { EventEmitter } from './events.js'; import { filterByRelevance } from './frameworks/framework.js'; import { frameworks } from './frameworks/index.js'; import { getFramework } from './get-framework.js'; import { report } from './metrics.js'; import { AVAILABLE_PACKAGE_MANAGERS, detectPackageManager, } from './package-managers/detect-package-manager.js'; import { runtimes } from './runtime/index.js'; import { getBuildSettings } from './settings/get-build-settings.js'; import { detectWorkspaces } from './workspaces/detect-workspace.js'; /** * The Project represents a Site in Netlify * The only configuration here needed is the path to the repository root and the optional baseDirectory */ export class Project { fs; /** An optional repository root that tells us where to stop looking up */ root; /** An absolute path */ baseDirectory; /** a relative base directory (like the path to workspace packages) */ relativeBaseDirectory; /** the detected package manager (if null it's not a javascript project, undefined indicates that id did not run yet) */ packageManager; /** an absolute path of the root directory for js workspaces where the most top package.json is located */ jsWorkspaceRoot; /* the detected javascript workspace information (if null it's not a workspace, undefined indicates that id did not run yet) */ workspace; /** The detected build-systems */ buildSystems; /** The combined build settings for a project, in a workspace there can be multiple settings per package */ settings; /** The detected frameworks for each path in a project */ frameworks; /** The detected language runtimes */ runtimes; /** a representation of the current environment */ #environment = {}; /** A bugsnag session */ bugsnag; /** A logging instance */ logger; /** A function that is used to report errors */ reportFn = report; events = new EventEmitter(); /** The current nodeVersion (can be set by node.js environments) */ _nodeVersion = null; setNodeVersion(version) { this._nodeVersion = parse(coerce(version), { loose: true }); return this; } async getCurrentNodeVersion() { if (this._nodeVersion) { return this._nodeVersion; } const nodeEnv = parse(coerce(this.getEnv('NODE_VERSION')), { loose: true }); if (nodeEnv) { return nodeEnv; } const nvmrc = await this.fs.gracefullyReadFile('.nvmrc'); if (nvmrc) { return parse(coerce(nvmrc, { loose: true })); } const nodeVersion = await this.fs.gracefullyReadFile('.node_version'); if (nodeVersion) { return parse(coerce(nodeVersion, { loose: true })); } // TODO: think of returning a default node version return null; } async isRedwoodProject() { return await this.fs.fileExists(this.fs.resolve(this.fs.cwd, 'redwood.toml')); } constructor(fs, baseDirectory, root) { this.fs = fs; this.baseDirectory = fs.resolve(root || '', baseDirectory !== undefined ? baseDirectory : fs.cwd); this.root = root ? fs.resolve(fs.cwd, root) : undefined; this.relativeBaseDirectory = baseDirectory !== undefined && !fs.isAbsolute(baseDirectory) ? baseDirectory : fs.relative(this.root || fs.cwd, this.baseDirectory); this.fs.cwd = this.baseDirectory; this.logger = fs.logger; } /** Set's the environment for the project */ setEnvironment(env) { this.#environment = env; return this; } /** Sets the function that is used to report errors. Overrides the default bugsnag reporting for the project */ setReportFn(fn) { this.reportFn = fn; return this; } /** Sets a bugsnag client for the current session */ setBugsnag(client) { if (client) { this.bugsnag = client; } return this; } /** retrieves an environment variable */ getEnv(key) { return this.#environment[key]; } /** Reports an error with additional metadata */ report(error, config = {}) { this.fs.logger.error(error); this.reportFn(error, { metadata: { build: { baseDirectory: this.baseDirectory, root: this.root, }, ...(config.metadata || {}), }, context: config.context, severity: config.severity, client: this.bugsnag, }); } /** Retrieve the run command for an npm script */ getNpmScriptCommand(npmScript) { const runCmd = this.packageManager?.runCommand || AVAILABLE_PACKAGE_MANAGERS.npm.runCommand; return `${runCmd} ${npmScript}`; } /** retrieves the root package.json file */ async getRootPackageJSON() { // get the most upper json file const rootJSONPath = (await this.fs.findUpMultiple('package.json', { cwd: this.baseDirectory, stopAt: this.root })).pop(); if (rootJSONPath) { this.jsWorkspaceRoot = this.fs.dirname(rootJSONPath); return this.fs.readJSON(rootJSONPath); } return {}; } /** Retrieves the package.json and if one found with it's pkgPath */ async getPackageJSON(startDirectory) { const pkgPath = await this.fs.findUp('package.json', { cwd: startDirectory || this.baseDirectory, stopAt: this.root, }); if (pkgPath) { const json = await this.fs.readJSON(pkgPath); return { ...json, pkgPath, }; } return { pkgPath: null }; } /** Resolves a path correctly with a package path, independent of run from the workspace root or from a package */ resolveFromPackage(packagePath, ...parts) { if (this.jsWorkspaceRoot) { return this.fs.join(this.jsWorkspaceRoot, packagePath, ...parts); } return this.baseDirectory.endsWith(packagePath) && !this.workspace?.isRoot ? this.fs.join(this.baseDirectory, ...parts) : this.fs.resolve(packagePath, ...parts); } /** Detects the used package Manager */ async detectPackageManager() { this.logger.debug('[project.ts]: detectPackageManager'); // if the packageManager is undefined, the detection was not run. // if it is an object or null it has already run if (this.packageManager !== undefined) { return this.packageManager; } try { this.packageManager = await detectPackageManager(this); await this.events.emit('detectPackageManager', this.packageManager); return this.packageManager; } catch { return null; } } /** Detects the javascript workspace settings */ async detectWorkspaces() { this.logger.debug('[project.ts]: detectWorkspaces'); // if the workspace is undefined, the detection was not run. // if it is an object or null it has already run if (this.workspace !== undefined) { return this.workspace; } // workspaces depend on package manager detection so run it first await this.detectPackageManager(); try { this.workspace = await detectWorkspaces(this); await this.events.emit('detectWorkspaces', this.workspace); return this.workspace; } catch (error) { this.report(error); return null; } } /** Detects all used build systems */ async detectBuildSystem() { this.logger.debug('[project.ts]: detectBuildSystem'); // if the workspace is undefined, the detection was not run. if (this.buildSystems !== undefined) { return this.buildSystems; } // build systems depends on workspaces detection await this.detectWorkspaces(); try { this.buildSystems = (await Promise.all(buildSystems.map((BuildSystem) => new BuildSystem(this).detect()))).filter(Boolean); await this.events.emit('detectBuildsystems', this.buildSystems); return this.buildSystems; } catch (error) { this.report(error); return []; } } /** Detects all used runtimes */ async detectRuntime() { this.logger.debug('[project.ts]: detectRuntime'); try { this.runtimes = (await Promise.all(runtimes.map((Runtime) => new Runtime(this).detect()))).filter(Boolean); this.events.emit('detectRuntimes', this.runtimes); return this.runtimes; } catch (error) { this.report(error); return []; } } /** Detects all used frameworks */ async detectFrameworks() { this.logger.debug('[project.ts]: detectFrameworks'); // if the workspace is undefined, the detection was not run. if (this.frameworks !== undefined) { return [...this.frameworks.values()].flat(); } try { // This needs to be run first await this.detectBuildSystem(); this.frameworks = new Map(); if (this.workspace) { // if we have a workspace parallelize in all workspaces await Promise.all(this.workspace.packages.map(async ({ path: pkg, forcedFramework }) => { if (forcedFramework) { try { const framework = await getFramework(forcedFramework, this); this.frameworks.set(pkg, [framework]); } catch { // noop framework not found } } else if (this.workspace) { const result = await this.detectFrameworksInPath(this.fs.join(this.workspace.rootDir, pkg)); this.frameworks.set(pkg, result); } })); } else { this.frameworks.set('', await this.detectFrameworksInPath()); } await this.events.emit('detectFrameworks', this.frameworks); return [...this.frameworks.values()].flat(); } catch (error) { this.report(error); return null; } } async detectFrameworksInPath(path) { this.logger.debug(`[project.ts]: detectFrameworksInPath - ${path}`); try { const detected = (await Promise.all(frameworks.map((Framework) => new Framework(this, path).detect()))).filter(Boolean); // sort based on the accuracy and drop un accurate results if something more accurate was found // from most accurate to least accurate // 1. a npm dependency was specified and matched // 2. only a config file was specified and matched // 3. an npm dependency was specified but matched over the config file (least accurate) // and prefer SSG over build tools return filterByRelevance(detected); } catch { return []; } } async getBuildSettings(packagePath) { this.logger.debug('[project.ts]: getBuildSettings'); // if the settings is undefined, the detection was not run. // if it is an array it has already run if (this.settings !== undefined) { return this.settings; } this.settings = []; try { // This needs to be run first await this.detectFrameworks(); this.settings = await getBuildSettings(this, packagePath); await this.events.emit('detectSettings', this.settings); } catch (error) { this.report(error); } return this.settings; } } //# sourceMappingURL=project.js.map