123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- 'use strict';
- const assert = require('assert');
- const fs = require('fs');
- const debug = require('debug')('egg-core:loader');
- const path = require('path');
- const globby = require('globby');
- const is = require('is-type-of');
- const deprecate = require('depd')('egg');
- const utils = require('../utils');
- const FULLPATH = Symbol('EGG_LOADER_ITEM_FULLPATH');
- const EXPORTS = Symbol('EGG_LOADER_ITEM_EXPORTS');
- const defaults = {
- directory: null,
- target: null,
- match: undefined,
- ignore: undefined,
- lowercaseFirst: false,
- caseStyle: 'camel',
- initializer: null,
- call: true,
- override: false,
- inject: undefined,
- filter: null,
- };
- /**
- * Load files from directory to target object.
- * @since 1.0.0
- */
- class FileLoader {
- /**
- * @class
- * @param {Object} options - options
- * @param {String|Array} options.directory - directories to be loaded
- * @param {Object} options.target - attach the target object from loaded files
- * @param {String} options.match - match the files when load, support glob, default to all js files
- * @param {String} options.ignore - ignore the files when load, support glob
- * @param {Function} options.initializer - custom file exports, receive two parameters, first is the inject object(if not js file, will be content buffer), second is an `options` object that contain `path`
- * @param {Boolean} options.call - determine whether invoke when exports is function
- * @param {Boolean} options.override - determine whether override the property when get the same name
- * @param {Object} options.inject - an object that be the argument when invoke the function
- * @param {Function} options.filter - a function that filter the exports which can be loaded
- * @param {String|Function} options.caseStyle - set property's case when converting a filepath to property list.
- */
- constructor(options) {
- assert(options.directory, 'options.directory is required');
- assert(options.target, 'options.target is required');
- this.options = Object.assign({}, defaults, options);
- // compatible old options _lowercaseFirst_
- if (this.options.lowercaseFirst === true) {
- deprecate('lowercaseFirst is deprecated, use caseStyle instead');
- this.options.caseStyle = 'lower';
- }
- }
- /**
- * attach items to target object. Mapping the directory to properties.
- * `app/controller/group/repository.js` => `target.group.repository`
- * @return {Object} target
- * @since 1.0.0
- */
- load() {
- const items = this.parse();
- const target = this.options.target;
- for (const item of items) {
- debug('loading item %j', item);
- // item { properties: [ 'a', 'b', 'c'], exports }
- // => target.a.b.c = exports
- item.properties.reduce((target, property, index) => {
- let obj;
- const properties = item.properties.slice(0, index + 1).join('.');
- if (index === item.properties.length - 1) {
- if (property in target) {
- if (!this.options.override) throw new Error(`can't overwrite property '${properties}' from ${target[property][FULLPATH]} by ${item.fullpath}`);
- }
- obj = item.exports;
- if (obj && !is.primitive(obj)) {
- obj[FULLPATH] = item.fullpath;
- obj[EXPORTS] = true;
- }
- } else {
- obj = target[property] || {};
- }
- target[property] = obj;
- debug('loaded %s', properties);
- return obj;
- }, target);
- }
- return target;
- }
- /**
- * Parse files from given directories, then return an items list, each item contains properties and exports.
- *
- * For example, parse `app/controller/group/repository.js`
- *
- * ```
- * module.exports = app => {
- * return class RepositoryController extends app.Controller {};
- * }
- * ```
- *
- * It returns a item
- *
- * ```
- * {
- * properties: [ 'group', 'repository' ],
- * exports: app => { ... },
- * }
- * ```
- *
- * `Properties` is an array that contains the directory of a filepath.
- *
- * `Exports` depends on type, if exports is a function, it will be called. if initializer is specified, it will be called with exports for customizing.
- * @return {Array} items
- * @since 1.0.0
- */
- parse() {
- let files = this.options.match;
- if (!files) {
- files = (process.env.EGG_TYPESCRIPT === 'true' && utils.extensions['.ts'])
- ? [ '**/*.(js|ts)', '!**/*.d.ts' ]
- : [ '**/*.js' ];
- } else {
- files = Array.isArray(files) ? files : [ files ];
- }
- let ignore = this.options.ignore;
- if (ignore) {
- ignore = Array.isArray(ignore) ? ignore : [ ignore ];
- ignore = ignore.filter(f => !!f).map(f => '!' + f);
- files = files.concat(ignore);
- }
- let directories = this.options.directory;
- if (!Array.isArray(directories)) {
- directories = [ directories ];
- }
- const filter = is.function(this.options.filter) ? this.options.filter : null;
- const items = [];
- debug('parsing %j', directories);
- for (const directory of directories) {
- const filepaths = globby.sync(files, { cwd: directory });
- for (const filepath of filepaths) {
- const fullpath = path.join(directory, filepath);
- if (!fs.statSync(fullpath).isFile()) continue;
- // get properties
- // app/service/foo/bar.js => [ 'foo', 'bar' ]
- const properties = getProperties(filepath, this.options);
- // app/service/foo/bar.js => service.foo.bar
- const pathName = directory.split(/[/\\]/).slice(-1) + '.' + properties.join('.');
- // get exports from the file
- const exports = getExports(fullpath, this.options, pathName);
- // ignore exports when it's null or false returned by filter function
- if (exports == null || (filter && filter(exports) === false)) continue;
- // set properties of class
- if (is.class(exports)) {
- exports.prototype.pathName = pathName;
- exports.prototype.fullPath = fullpath;
- }
- items.push({ fullpath, properties, exports });
- debug('parse %s, properties %j, export %j', fullpath, properties, exports);
- }
- }
- return items;
- }
- }
- module.exports = FileLoader;
- module.exports.EXPORTS = EXPORTS;
- module.exports.FULLPATH = FULLPATH;
- // convert file path to an array of properties
- // a/b/c.js => ['a', 'b', 'c']
- function getProperties(filepath, { caseStyle }) {
- // if caseStyle is function, return the result of function
- if (is.function(caseStyle)) {
- const result = caseStyle(filepath);
- assert(is.array(result), `caseStyle expect an array, but got ${result}`);
- return result;
- }
- // use default camelize
- return defaultCamelize(filepath, caseStyle);
- }
- // Get exports from filepath
- // If exports is null/undefined, it will be ignored
- function getExports(fullpath, { initializer, call, inject }, pathName) {
- let exports = utils.loadFile(fullpath);
- // process exports as you like
- if (initializer) {
- exports = initializer(exports, { path: fullpath, pathName });
- }
- // return exports when it's a class or generator
- //
- // module.exports = class Service {};
- // or
- // module.exports = function*() {}
- if (is.class(exports) || is.generatorFunction(exports) || is.asyncFunction(exports)) {
- return exports;
- }
- // return exports after call when it's a function
- //
- // module.exports = function(app) {
- // return {};
- // }
- if (call && is.function(exports)) {
- exports = exports(inject);
- if (exports != null) {
- return exports;
- }
- }
- // return exports what is
- return exports;
- }
- function defaultCamelize(filepath, caseStyle) {
- const properties = filepath.substring(0, filepath.lastIndexOf('.')).split('/');
- return properties.map(property => {
- if (!/^[a-z][a-z0-9_-]*$/i.test(property)) {
- throw new Error(`${property} is not match 'a-z0-9_-' in ${filepath}`);
- }
- // use default camelize, will capitalize the first letter
- // foo_bar.js > FooBar
- // fooBar.js > FooBar
- // FooBar.js > FooBar
- // FooBar.js > FooBar
- // FooBar.js > fooBar (if lowercaseFirst is true)
- property = property.replace(/[_-][a-z]/ig, s => s.substring(1).toUpperCase());
- let first = property[0];
- switch (caseStyle) {
- case 'lower':
- first = first.toLowerCase();
- break;
- case 'upper':
- first = first.toUpperCase();
- break;
- case 'camel':
- default:
- }
- return first + property.substring(1);
- });
- }
|