browsertime
Version:
Get performance metrics from your web page using Browsertime.
195 lines (178 loc) • 5.97 kB
JavaScript
import fs from 'node:fs';
import path from 'node:path';
import { promisify } from 'node:util';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
import intel from 'intel';
import { onlyFiles, onlyWithExtension, onlyDirectories } from './filters.js';
const require = createRequire(import.meta.url);
const readdir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
const stat = promisify(fs.stat);
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const rootFolder = path.resolve(__dirname, '..', '..', 'browserscripts');
const log = intel.getLogger('browsertime');
function toFullPath(filename, root) {
return path.join(root, filename);
}
/**
* Read all JavaScript files in a specific dir. Will use the filename
* without .js as the name of the script.
* @param {*} dirpath
*/
async function scriptsFromDirectory(dirpath) {
const dir = await readdir(dirpath);
const cleaned = dir
.map(name => toFullPath(name, dirpath))
.filter(await onlyFiles)
.filter(onlyWithExtension('.js'));
const result = {};
for (const filepath of cleaned) {
const name = path.basename(filepath, '.js');
const script = await readFile(filepath, 'utf8');
result[name] = generateScriptObject(name, filepath, script);
}
return result;
}
async function getScriptsForCategories(categories) {
const directories = await readdir(rootFolder);
const cleaned = directories
.filter(name => categories.find(n => n === name))
.map(name => toFullPath(name, rootFolder))
.filter(await onlyDirectories);
const result = {};
for (const dir of cleaned) {
const name = path.basename(dir);
const scripts = await scriptsFromDirectory(dir);
result[name] = scripts;
}
return result;
}
/**
* Take the name of a script, its location on the disk and its contents and
* generate a script object:
*
* name: The name of the script.
* function: A function object if this is a new-style script; null otherwise.
* content: The source code of the function (contents) if this is a old-style
* script; null otherwise.
* requires: An object with boolean fields indictating which requirements
* must be met to execute this new-style function; the object is empty
* for an old-style function.
*
* @param {string}
* @param {string}
* @param {string}
* @returns {ScriptObject}
*/
function generateScriptObject(name, path, contents) {
try {
const scriptAndMetadataObject = require(path);
if (
typeof scriptAndMetadataObject.function === 'function' &&
typeof scriptAndMetadataObject.requires === 'object'
) {
log.verbose(name + ' is a new-style script object.');
return {
name: name,
requires: scriptAndMetadataObject.requires,
function: scriptAndMetadataObject.function,
content: undefined,
isAsync:
Object.getPrototypeOf(scriptAndMetadataObject.function) ===
Object.getPrototypeOf(async function () {})
};
}
} catch {
// Use this as a signal to fall back to an old-style script, but don't
// perform any action
}
if (log.isEnabledFor(log.TRACE)) {
log.verbose(name + ' is an old-style script object.');
}
return {
name: name,
content: contents,
function: undefined,
requires: {}
};
}
/**
* Parse a file or directory, and return an object representing that groups scripts by category.
* Single js files will be put into the category 'custom', for directories the category name will
* be taken from the directory name.
*
* The resulting value looks like this:
* <pre>
* {
* 'category': {
* 'script': <contents of script.js>
* }
* }
* </pre>
* @param {string} root a path to a js file, alternatively a directory containing js files.
* @param {string} category a category to apply if a file is passed
* @returns {Promise.<Object>}
*/
async function findAndParseScripts(root, category) {
async function scriptsFromDirectory(dirpath) {
const dir = await readdir(dirpath);
const cleaned = dir
.map(name => toFullPath(name, dirpath))
.filter(await onlyFiles)
.filter(onlyWithExtension('.js'));
const result = {};
for (const filepath of cleaned) {
const name = path.basename(filepath, '.js');
const script = await readFile(filepath, 'utf8');
result[name] = generateScriptObject(name, root, script);
}
return result;
}
async function parseDirectory(dirPath) {
const categoryName = path.basename(dirPath);
return scriptsFromDirectory(dirPath).then(scripts => {
const categories = {};
categories[categoryName] = scripts;
return categories;
});
}
return stat(root).then(stats => {
return stats.isFile()
? readFile(root, 'utf8').then(content => {
const name = path.basename(root, '.js');
let scripts = {};
scripts[name] = generateScriptObject(name, root, content);
let categories = {};
categories[category] = scripts;
return categories;
})
: parseDirectory(root);
});
}
const defaultScriptCategories = ['browser', 'pageinfo', 'timings'];
// FIXME need to add async here, is that possible for properties?
async function allScriptCategories() {
return readdir(rootFolder).then(files => {
files = files.map(fileName => path.join(rootFolder, fileName));
const p = [];
for (const file of files) {
p.push(onlyDirectories(file));
}
return Promise.all(p).then(isDirectories => {
const onlyDirectories = [];
for (const [index, isDirectory] of isDirectories.entries()) {
if (isDirectory) {
onlyDirectories.push(files[index]);
}
}
return onlyDirectories.map(dirName => path.basename(dirName));
});
});
}
export {
defaultScriptCategories,
allScriptCategories,
getScriptsForCategories,
findAndParseScripts
};