UNPKG

@netlify/build-info

Version:
251 lines 10.3 kB
import { coerce, parse } from 'semver'; import { getBuildCommands, getDevCommands } from '../get-commands.js'; export var Category; (function (Category) { 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 = {})); /** 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) { let sort = a.detected.accuracy > b.detected.accuracy ? -1 : a.detected.accuracy < b.detected.accuracy ? 1 : 0; if (sort >= 0) { // prefer SSG over build tools if (a.category === Category.SSG && b.category === Category.BuildTool) { sort--; } } 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 = [...Object.entries(pkgJSON.dependencies || {}), ...Object.entries(pkgJSON.devDependencies || {})]; const found = allDeps.find(([depName]) => this.npmDependencies.includes(depName)); // check for excluded dependencies const excluded = allDeps.some(([depName]) => this.excludedNpmDependencies.includes(depName)); if (!excluded && found?.[0]) { const version = await this.getVersionFromNodeModules(found[0]); return { name: found[0], // coerce to parse syntax like ~0.1.2 or ^1.2.3 version: version || parse(coerce(found[1])) || 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', }, 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