UNPKG

@netlify/build-info

Version:

Build info utility

286 lines 11.9 kB
import { coerce, parse } from 'semver'; import { getBuildCommands, getDevCommands } from '../get-commands.js'; export var Category; (function (Category) { Category["BackendFramework"] = "backend_framework"; Category["FrontendFramework"] = "frontend_framework"; Category["SSG"] = "static_site_generator"; Category["BuildTool"] = "build_tool"; })(Category || (Category = {})); export var Accuracy; (function (Accuracy) { Accuracy[Accuracy["Forced"] = 5] = "Forced"; Accuracy[Accuracy["NPM"] = 4] = "NPM"; Accuracy[Accuracy["ConfigOnly"] = 3] = "ConfigOnly"; Accuracy[Accuracy["Config"] = 2] = "Config"; Accuracy[Accuracy["NPMHoisted"] = 1] = "NPMHoisted"; })(Accuracy || (Accuracy = {})); export var VersionAccuracy; (function (VersionAccuracy) { VersionAccuracy["NodeModules"] = "node_modules"; VersionAccuracy["PackageJSONPinned"] = "package_json_pinned"; VersionAccuracy["PackageJSON"] = "package_json"; })(VersionAccuracy || (VersionAccuracy = {})); /** Filters a list of detected frameworks by relevance, meaning we drop build tools if we find static site generators */ export function filterByRelevance(detected) { const filtered = []; for (const framework of detected.sort(sortFrameworksBasedOnAccuracy)) { // only keep the frameworks on the highest accuracy level. (so if multiple SSG are detected use them but drop build tools) if (filtered.length === 0 || filtered[0].detected.accuracy === framework.detected.accuracy) { filtered.push(framework); } } return filtered; } /** * sort a list of frameworks based on the accuracy and on it's type (prefer static site generators over build tools) * 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) */ export function sortFrameworksBasedOnAccuracy(a, b) { const sort = b.detected.accuracy - a.detected.accuracy; // Secondary sorting on Category if (sort === 0) { const categoryRanking = [Category.BackendFramework, Category.FrontendFramework, Category.BuildTool, Category.SSG]; return categoryRanking.indexOf(b.category) - categoryRanking.indexOf(a.category); } return sort; } /** Merges a list of detection results based on accuracy to get the one with the highest accuracy that still contains information provided by all other detections */ export function mergeDetections(detections) { const definedDetections = detections .filter(function isDetection(d) { return Boolean(d); }) .sort((a, b) => (a.accuracy > b.accuracy ? -1 : a.accuracy < b.accuracy ? 1 : 0)); if (definedDetections.length === 0) { return; } return definedDetections.slice(1).reduce((merged, detection) => { merged.config = merged.config ?? detection.config; merged.configName = merged.configName ?? detection.configName; merged.package = merged.package ?? detection.package; merged.packageJSON = merged.packageJSON ?? detection.packageJSON; return merged; }, definedDetections[0]); } export class BaseFramework { project; path; id; name; category; detected; version; configFiles = []; npmDependencies = []; excludedNpmDependencies = []; plugins = []; staticAssetsDirectory; env = {}; dev; build = { command: 'npm run build', directory: 'dist', }; logo; constructor( /** The current project inside we want to detect the framework */ project, /** An absolute path considered as the baseDirectory for detection, prefer that over the project baseDirectory */ path) { this.project = project; this.path = path; if (project.packageManager?.runCommand) { this.build.command = `${project.packageManager.runCommand} build`; } } setDetected(accuracy, reason) { this.detected = { accuracy, }; if (typeof reason === 'string') { this.detected.config = reason; } else { this.detected.package = reason; } return this; } /** Retrieves the version of a npm package from the node_modules */ async getVersionFromNodeModules(packageName) { // on the browser we can omit this check if (this.project.fs.getEnvironment() === "browser" /* Environment.Browser */) { return; } try { const packageJson = await this.project.fs.findUp(this.project.fs.join('node_modules', packageName, 'package.json'), { cwd: this.path || this.project.baseDirectory, stopAt: this.project.root, }); if (packageJson) { const { version } = await this.project.fs.readJSON(packageJson); if (typeof version === 'string') { return parse(version) || undefined; } } } catch { // noop } } /** check if the npmDependencies are used inside the provided package.json */ async npmDependenciesUsed(pkgJSON) { const allDeps = { ...(pkgJSON.dependencies ?? {}), ...(pkgJSON.devDependencies ?? {}), }; const matchedDepName = Object.keys(allDeps).find((depName) => this.npmDependencies.includes(depName)); const hasExcludedDeps = Object.keys(allDeps).some((depName) => this.excludedNpmDependencies.includes(depName)); if (!hasExcludedDeps && matchedDepName != null) { const versionFromNodeModules = await this.getVersionFromNodeModules(matchedDepName); if (versionFromNodeModules) { return { name: matchedDepName, version: versionFromNodeModules, versionAccuracy: VersionAccuracy.NodeModules, }; } const matchedDepVersion = allDeps[matchedDepName]; // Try to parse without coercing first to detect pinned versions (e.g., "1.2.3") const pinnedVersion = parse(matchedDepVersion); if (pinnedVersion) { return { name: matchedDepName, version: pinnedVersion, versionAccuracy: VersionAccuracy.PackageJSONPinned, }; } // Coerce to parse syntax like ~0.1.2 or ^1.2.3 const coercedVersion = parse(coerce(matchedDepVersion)) || undefined; if (coercedVersion) { return { name: matchedDepName, version: coercedVersion, versionAccuracy: VersionAccuracy.PackageJSON, }; } return { name: matchedDepName, version: undefined, versionAccuracy: undefined, }; } } /** detect if the framework occurs inside the package.json dependencies */ async detectNpmDependency() { if (this.npmDependencies.length) { const startDir = this.path || this.project.baseDirectory; const pkg = await this.project.getPackageJSON(startDir); const dep = await this.npmDependenciesUsed(pkg); if (dep) { this.version = dep.version; return { // if the match of the npm package was in a directory up we don't have a high accuracy accuracy: this.project.fs.join(startDir, 'package.json') === pkg.pkgPath ? Accuracy.NPM : Accuracy.NPMHoisted, package: dep, packageJSON: pkg, }; } } } /** detect if the framework config file is located somewhere up the tree */ async detectConfigFile(configFiles) { if (configFiles.length) { const config = await this.project.fs.findUp(configFiles, { cwd: this.path || this.project.baseDirectory, stopAt: this.project.root, }); if (config) { return { // Have higher trust on a detection of a config file if there is no npm dependency specified for this framework // otherwise the npm dependency should have already triggered the detection accuracy: this.npmDependencies.length === 0 ? Accuracy.ConfigOnly : Accuracy.Config, config, configName: this.project.fs.basename(config), }; } } } /** * Checks if the project is using a specific framework: * - if `npmDependencies` is set, one of them must be present in the `package.json` `dependencies|devDependencies` * - if `excludedNpmDependencies` is set, none of them must be present in the `package.json` `dependencies|devDependencies` * - if `configFiles` is set, one of the files must exist */ async detect() { const npm = await this.detectNpmDependency(); const config = await this.detectConfigFile(this.configFiles ?? []); this.detected = mergeDetections([ // we can force frameworks as well this.detected?.accuracy === Accuracy.Forced ? this.detected : undefined, npm, config, ]); if (this.detected) { return this; } // nothing detected } /** * Retrieve framework's dev commands. * We use, in priority order: * - `package.json` `scripts` containing the frameworks dev command * - `package.json` `scripts` whose names are among a list of common dev scripts like: `dev`, `serve`, `develop`, ... * - The frameworks dev command */ getDevCommands() { // Some frameworks don't have a dev command if (this.dev?.command === undefined) { return []; } const devCommands = getDevCommands(this.dev.command, this.detected?.packageJSON?.scripts); if (devCommands.length > 0) { return devCommands.map((command) => this.project.getNpmScriptCommand(command)); } return [this.dev.command]; } getBuildCommands() { const buildCommands = getBuildCommands(this.build.command, this.detected?.packageJSON?.scripts); if (buildCommands.length > 0) { return buildCommands.map((command) => this.project.getNpmScriptCommand(command)); } return [this.build.command]; } /** This method will be called by the JSON.stringify */ toJSON() { return { id: this.id, name: this.name, package: { name: this.detected?.package?.name || this.npmDependencies?.[0], version: this.detected?.package?.version?.raw || 'unknown', versionAccuracy: this.detected?.package?.versionAccuracy, }, category: this.category, dev: { commands: this.getDevCommands(), port: this.dev?.port, pollingStrategies: this.dev?.pollingStrategies, }, build: { commands: [this.build.command], directory: this.build.directory, }, staticAssetsDirectory: this.staticAssetsDirectory, env: this.env, logo: this.logo ? Object.entries(this.logo).reduce((prev, [key, value]) => ({ ...prev, [key]: `https://framework-info.netlify.app${value}` }), {}) : undefined, plugins: this.plugins, }; } } //# sourceMappingURL=framework.js.map