UNPKG

@overlook/plugin-load

Version:

Overlook framework file system loader plugin

371 lines (329 loc) 11.9 kB
/* -------------------- * @overlook/plugin-load module * Entry point * ------------------*/ 'use strict'; // Modules const {join: pathJoin, sep: pathSep} = require('path'), invariant = require('simple-invariant'), {readdir, stat} = require('fs-extra'), Plugin = require('@overlook/plugin'), {isRoute, isRouteClass, INIT_PROPS, DEBUG_ERROR} = require('@overlook/route'), {File} = require('@overlook/plugin-fs'), {PRE_BUILD, deleteRouteProperties} = require('@overlook/plugin-build'), {isString} = require('is-it-type'); // Imports const loadFile = require('./loadFile.js'), pkg = require('../package.json'); // Constants const DEFAULT_DIR_INDEX = 'index', DEFAULT_ROUTE_EXTS = ['route.js', 'route.mjs', 'route.cjs', 'js', 'mjs', 'cjs']; // Exports const loadPlugin = new Plugin( pkg, { symbols: [ // Public methods 'LOAD', // Props to be set by user 'DIR_INDEX', 'ROUTE_EXTS', 'PARENT_PATH', // Props for other plugins to read 'FILES', // Methods for user/other plugins to override 'GET_DIR_INDEX', 'GET_ROUTE_EXTS', 'GET_PARENT_PATH', 'IDENTIFY_ROUTE_FILE', // Internal methods 'LOAD_DIR', 'LOAD_FILE', 'ATTACH_FILE', 'LOAD_FILES', 'LOAD_DIRS', // Internal props 'SRC_PATH', 'SRC_DIR_PATH', 'SRC_FILENAME' ] }, (Route, { LOAD, DIR_INDEX, ROUTE_EXTS, PARENT_PATH, FILES, GET_DIR_INDEX, GET_ROUTE_EXTS, GET_PARENT_PATH, IDENTIFY_ROUTE_FILE, LOAD_DIR, LOAD_FILE, ATTACH_FILE, LOAD_FILES, LOAD_DIRS, SRC_PATH, SRC_DIR_PATH, SRC_FILENAME }) => class LoadRoute extends Route { /** * Init props used by plugin. * @param {Object} props - Props */ [INIT_PROPS](props) { super[INIT_PROPS](props); this[DIR_INDEX] = undefined; this[ROUTE_EXTS] = undefined; this[PARENT_PATH] = undefined; this[FILES] = undefined; this[SRC_PATH] = undefined; this[SRC_DIR_PATH] = undefined; this[SRC_FILENAME] = undefined; } /** * Load directory. * Called on index route of directory *above* the directory being loaded. * @param {string} path - Path to dir to load * @param {string} [name] - Name for root route (default 'root') * @returns {Route} - Root route */ [LOAD](path, name) { return this[LOAD_DIR](path, name || 'root', Object.create(null)); } /** * Load directory. * Called on index route of directory *above* the directory being loaded. * @param {string} dirPath - Path of dir to load * @param {string} dirName - Name for route * @param {Object} routes - Dictionary of routes, keyed by path * @returns {Route} - Index route for dir */ async [LOAD_DIR](dirPath, dirName, routes) { // Get files in dir const filenames = await readdir(dirPath); filenames.sort(); // Split into files and dirs and group together files by name // i.e. `foo.js`, `foo.jsx`, `foo.test.js` together under 'foo' const filesMap = Object.create(null), subDirs = []; for (const filename of filenames) { const filePath = pathJoin(dirPath, filename); const statObj = await stat(filePath); if (statObj.isDirectory()) { // Directory subDirs.push({name: filename, path: filePath}); continue; } // File - split name + ext const match = filename.match(/^([^.]*)(?:\.(.*))?$/), name = match[1], ext = match[2] || ''; // Add to files let file = filesMap[name]; if (!file) { file = {name, routeName: name, exts: Object.create(null)}; filesMap[name] = file; } file.exts[ext] = new File(filePath); } // Identify index for this dir let indexName = this[DIR_INDEX]; if (!indexName) { indexName = this[GET_DIR_INDEX](); if (!indexName) indexName = DEFAULT_DIR_INDEX; } // Identify index route file. // If no file, create dummy one with no exts. let file = filesMap[indexName]; if (!file) { file = {name: indexName, routeName: dirName, exts: Object.create(null)}; } else { file.routeName = dirName; } // Load file const route = await this[LOAD_FILE](file, dirPath, true, routes); if (!route) return null; this[ATTACH_FILE](route, dirPath, true, routes); // Load the dir via index route (exclude index file from list of files passed to [LOAD_FILES]) if (route[LOAD_FILES]) { const files = []; for (const name in filesMap) { if (name !== indexName) files.push(filesMap[name]); } await route[LOAD_FILES](files, routes); } if (route[LOAD_DIRS]) await route[LOAD_DIRS](subDirs, routes); return route; } /** * Load group of files as a route. * Called on index route of directory *containing* the files being loaded. * @param {Object} file - File object of form `{name, routeName, exts: {}}` * @param {string} dirPath - Path of directory file is in * @param {boolean} isIndex - `true` if is directory index * @param {Object} routes - Dictionary of routes, keyed by path * @returns {Route|null} - Route, or `null` if cannot be loaded */ async [LOAD_FILE](file, dirPath, isIndex, routes) { const {routeName, exts} = file; const identified = this[IDENTIFY_ROUTE_FILE](exts, isIndex, routeName); if (identified == null) return null; let route, path; if (isString(identified)) { // Load route file const routeFile = exts[identified]; invariant(routeFile, `ext returned from [IDENTIFY_ROUTE_FILE] '${identified}' is not present`); path = routeFile.path; route = await loadFile(path); invariant(isRoute(route), `file at '${path}' located via [IDENTIFY_ROUTE_FILE] is not a Route`); } else if (isRouteClass(identified)) { route = new identified(); // eslint-disable-line new-cap } else { invariant( isRoute(identified), 'return value from [IDENTIFY_ROUTE_FILE] must be a Route, a Route class or a string' ); route = identified; } // Add props to route const {name} = file; if (!route.name) route.name = routeName; route[FILES] = exts; if (path) route[SRC_PATH] = path; route[SRC_DIR_PATH] = dirPath; route[SRC_FILENAME] = name; // Add to routes map routes[pathJoin(dirPath, name)] = route; if (isIndex) routes[`${dirPath}${pathSep}`] = route; return route; } /** * Attach route to its parent route. * Called on index route of directory *containing* the files being loaded. * @param {Route} route - Route * @param {string} dirPath - Path of dir this route loaded from * @param {boolean} isIndex - `true` if is directory index * @param {Object} routes - Dictionary of routes, keyed by path * @returns {undefined} */ [ATTACH_FILE](route, dirPath, isIndex, routes) { // Find parent let parentPath = route[PARENT_PATH]; let parent; if (parentPath == null && route[GET_PARENT_PATH]) parentPath = route[GET_PARENT_PATH](isIndex); if (parentPath != null) { // Conform parent path (remove trailing slashes) const match = parentPath.match(/^((?:\.?\.\/)*)(.*?)(\/?)$/); invariant(match, `Invalid parent path '${parentPath}' for '${route[SRC_PATH]}'`); const [, prefixDots, body, endSlash] = match; if (endSlash && body) parentPath = parentPath.slice(0, -1); if (!prefixDots) { parentPath = ['.', '..'].includes(body) ? `${parentPath}/` : `./${parentPath}`; } // Find parent if (pathSep === '\\') parentPath = parentPath.replace(/\//g, '\\'); parent = routes[pathJoin(dirPath, parentPath)]; invariant(parent, `Cannot locate parent path '${parentPath}' for '${route[SRC_PATH]}'`); } else { // Default parent is the route loading it parentPath = isIndex ? '../' : './'; parent = this; } route[PARENT_PATH] = parentPath; // Attach route to parent parent.attachChild(route); } /** * Load files in directory. * Called on index route of directory *containing* the files. * @param {Array<Object>} files - File objects of form `{name, routeName, exts: {}}` * @param {Object} routes - Dictionary of routes, keyed by path * @returns {undefined} */ async [LOAD_FILES](files, routes) { // Load all files const dirPath = this[SRC_DIR_PATH]; const newRoutes = []; for (const file of files) { const route = await this[LOAD_FILE](file, dirPath, false, routes); if (route) newRoutes.push(route); } // Attach routes to parents. // This is done in a 2nd iteration in case routes loaded earlier have a later route as parent. for (const route of newRoutes) { this[ATTACH_FILE](route, dirPath, false, routes); } } /** * Load set of sub-directories. * Called on index route of directory *containing* these sub-directories. * @param {Array<Object>} dirs - Dir objects of form `{name, path}` * @param {Object} routes - Dictionary of routes, keyed by path * @returns {undefined} */ async [LOAD_DIRS](dirs, routes) { for (const {path, name} of dirs) { await this[LOAD_DIR](path, name, routes); } } /** * Identify which of files is the route file * or specify Route class to use to create route from. * Can be overridden by subclasses. * @param {Object} exts - Object keyed by file extension * @param {boolean} isIndex - `true` if is directory index * @param {string} name - Route name * @returns {string|Object|null|undefined} - If file identified, * return ext as string, Route or Route class */ [IDENTIFY_ROUTE_FILE](exts, isIndex, name) { // eslint-disable-line no-unused-vars // Get valid file extensions for route files let routeExts = this[ROUTE_EXTS]; if (!routeExts) { routeExts = this[GET_ROUTE_EXTS](); if (!routeExts) routeExts = DEFAULT_ROUTE_EXTS; } // Identify if one of files has this extension for (const ext of routeExts) { if (exts[ext]) return ext; } return undefined; } /** * Add load path to error messages. * @param {Error} err - Error * @returns {Error} - Error with extra props */ [DEBUG_ERROR](err) { err = super[DEBUG_ERROR](err); const srcPath = this[SRC_PATH]; if (srcPath) { err[SRC_PATH] = srcPath; err.message += ` (source path ${srcPath})`; } return err; } /** * Identify which of files in a dir is the index file for that dir. * Intended to be overridden by subclasses. * @returns {string|null|undefined} - name */ [GET_DIR_INDEX]() { // eslint-disable-line class-methods-use-this return undefined; } /** * Identify which file extensions are used for route files. * Intended to be overridden by subclasses. * @returns {Array<string>|null|undefined} - Array of extensions */ [GET_ROUTE_EXTS]() { // eslint-disable-line class-methods-use-this return undefined; } /** * Get parent path for this route. * Intended to be overridden by subclasses. * @param {boolean} isIndex - `true` if file is directory index * @returns {string|null|undefined} - Parent path (e.g. './') */ [GET_PARENT_PATH](isIndex) { // eslint-disable-line class-methods-use-this, no-unused-vars return undefined; } /** * If app is being built, delete all methods and properties which are not used at run time. * This comprises all methods and properties except `[DEBUG_ERROR]()`. * It's particularly important to delete `[LOAD_FILE]()` method as it contains a reference * to `loadFile()` which contains a `require()`, which cannot be serialized by Livepack, * so would cause build to fail. * @returns {undefined} */ async [PRE_BUILD]() { if (super[PRE_BUILD]) await super[PRE_BUILD](); deleteRouteProperties(this, [ LOAD, LOAD_DIR, LOAD_FILE, ATTACH_FILE, LOAD_FILES, LOAD_DIRS, IDENTIFY_ROUTE_FILE, GET_DIR_INDEX, GET_ROUTE_EXTS, GET_PARENT_PATH, DIR_INDEX, ROUTE_EXTS, PARENT_PATH, FILES, SRC_PATH, SRC_DIR_PATH, SRC_FILENAME ]); } } ); loadPlugin.File = File; module.exports = loadPlugin;