@netlify/build-info
Version:
Build info utility
251 lines • 10.3 kB
JavaScript
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