From 9aaeaa404a7eee2f3bc19a5069c62a8b08f8c859 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sat, 15 Jun 2024 13:31:19 +0800 Subject: [PATCH] FileLoader --- package.json | 12 +- src/egg.ts | 32 +-- src/lifecycle.ts | 8 +- src/loader/base_loader.ts | 42 +-- src/loader/egg_loader.ts | 6 +- src/loader/{file_loader.js => file_loader.ts} | 131 +++++---- src/loader/mixin/{config.js => config.ts} | 23 +- src/loader/mixin/plugin.ts | 6 +- src/utils/index.ts | 47 ++-- src/utils/sequencify.ts | 39 +-- ...ile_loader.test.js => file_loader.test.ts} | 258 +++++++++--------- test/utils.ts | 28 +- tsconfig.json | 16 +- 13 files changed, 344 insertions(+), 304 deletions(-) rename src/loader/{file_loader.js => file_loader.ts} (67%) rename src/loader/mixin/{config.js => config.ts} (89%) rename test/loader/{file_loader.test.js => file_loader.test.ts} (53%) diff --git a/package.json b/package.json index d2e029aa..0fce70b5 100644 --- a/package.json +++ b/package.json @@ -30,16 +30,12 @@ "dependencies": { "@eggjs/koa": "^2.18.1", "@eggjs/router": "^3.0.0", - "@types/depd": "^1.1.32", - "co": "^4.6.0", - "depd": "^2.0.0", "egg-logger": "^3.5.0", "egg-path-matching": "^1.1.0", - "extend2": "^1.0.1", + "extend2": "^4.0.0", "get-ready": "^3.1.0", "globby": "^11.0.2", "is-type-of": "^2.1.0", - "koa-convert": "^1.2.0", "node-homedir": "^2.0.0", "ready-callback": "^4.0.0", "tsconfig-paths": "^4.1.1", @@ -47,6 +43,7 @@ }, "devDependencies": { "@eggjs/tsconfig": "1", + "@types/js-yaml": "^4.0.9", "@types/mocha": "10", "@types/node": "20", "await-event": "2", @@ -59,14 +56,13 @@ "git-contributor": "2", "js-yaml": "^3.13.1", "mm": "3", - "pedding": "^1.1.0", "spy": "^1.0.0", "supertest": "^4.0.2", "ts-node": "10", - "urllib": "3", "tshy": "^1.15.1", "tshy-after": "^1.0.0", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "urllib": "3" }, "publishConfig": { "access": "public" diff --git a/src/egg.ts b/src/egg.ts index 38eda988..034e8664 100644 --- a/src/egg.ts +++ b/src/egg.ts @@ -1,17 +1,17 @@ import assert from 'node:assert'; -import fs from 'node:fs'; +// import fs from 'node:fs'; import { debuglog } from 'node:util'; import is from 'is-type-of'; import KoaApplication, { type MiddlewareFunc } from '@eggjs/koa'; import { EggConsoleLogger } from 'egg-logger'; import { EggRouter as Router } from '@eggjs/router'; import type { ReadyFunctionArg } from 'get-ready'; -import { BaseContextClass } from './utils/base_context_class'; -import utils from './utils'; -import { Timing } from './utils/timing'; -import type { Fun } from './utils'; -import { Lifecycle } from './lifecycle'; -import type { EggLoader } from './loader/egg_loader'; +import { BaseContextClass } from './utils/base_context_class.js'; +import utils from './utils/index.js'; +import { Timing } from './utils/timing.js'; +import type { Fun } from './utils/index.js'; +import { Lifecycle } from './lifecycle.js'; +import { EggLoader, EggLoaderMixin } from './loader/egg_loader.js'; const debug = debuglog('@eggjs/core:egg'); @@ -36,7 +36,7 @@ export class EggCore extends KoaApplication { Controller: typeof BaseContextClass; Service: typeof BaseContextClass; lifecycle: Lifecycle; - loader: EggLoader; + loader: EggLoaderMixin; /** * @class @@ -47,17 +47,15 @@ export class EggCore extends KoaApplication { * @since 1.0.0 */ constructor(options: Partial = {}) { - options.baseDir = options.baseDir || process.cwd(); - options.type = options.type || 'application'; - + options.baseDir = options.baseDir ?? process.cwd(); + options.type = options.type ?? 'application'; assert(typeof options.baseDir === 'string', 'options.baseDir required, and must be a string'); - assert(fs.existsSync(options.baseDir), `Directory ${options.baseDir} not exists`); - assert(fs.statSync(options.baseDir).isDirectory(), `Directory ${options.baseDir} is not a directory`); + // assert(fs.existsSync(options.baseDir), `Directory ${options.baseDir} not exists`); + // assert(fs.statSync(options.baseDir).isDirectory(), `Directory ${options.baseDir} is not a directory`); assert(options.type === 'application' || options.type === 'agent', 'options.type should be application or agent'); super(); this.timing = new Timing(); - // cache deprecate object by file this[DEPRECATE] = new Map(); @@ -137,8 +135,8 @@ export class EggCore extends KoaApplication { plugins: options.plugins, logger: this.console, serverScope: options.serverScope, - env: options.env, - }); + env: options.env ?? '', + }) as unknown as EggLoaderMixin; } /** @@ -336,7 +334,7 @@ export class EggCore extends KoaApplication { } get [EGG_LOADER]() { - return require('./loader/egg_loader'); + return EggLoader; } } diff --git a/src/lifecycle.ts b/src/lifecycle.ts index 660a71d3..a02fdfbb 100644 --- a/src/lifecycle.ts +++ b/src/lifecycle.ts @@ -6,9 +6,9 @@ import ReadyObject from 'get-ready'; import type { ReadyFunctionArg } from 'get-ready'; import { Ready } from 'ready-callback'; import { EggConsoleLogger } from 'egg-logger'; -import utils from './utils'; -import type { Fun } from './utils'; -import type { EggCore } from './egg'; +import utils from './utils/index.js'; +import type { Fun } from './utils/index.js'; +import type { EggCore } from './egg.js'; const debug = debuglog('@eggjs/core:lifecycle'); @@ -141,7 +141,7 @@ export class Lifecycle extends EventEmitter { this.#bootHooks.push(BootClass); } - addFunctionAsBootHook(hook: (app: T) => void) { + addFunctionAsBootHook(hook: (app: T) => void) { assert(this.#init === false, 'do not add hook when lifecycle has been initialized'); // app.js is exported as a function // call this function in configDidLoad diff --git a/src/loader/base_loader.ts b/src/loader/base_loader.ts index 36dec67c..13108946 100644 --- a/src/loader/base_loader.ts +++ b/src/loader/base_loader.ts @@ -5,7 +5,7 @@ import { debuglog } from 'node:util'; import is from 'is-type-of'; import homedir from 'node-homedir'; import type { Logger } from 'egg-logger'; -import utility from 'utility'; +import { readJSONSync } from 'utility'; import FileLoader from './file_loader'; import ContextLoader from './context_loader'; import utils from '../utils'; @@ -65,6 +65,11 @@ export interface EggLoaderOptions { plugins?: Record; } +export interface EggDirInfo { + path: string; + type: 'app' | 'plugin' | 'framework'; +} + export class BaseLoader { #requiredCount: 0; readonly options: EggLoaderOptions; @@ -74,6 +79,7 @@ export class BaseLoader { readonly serverEnv: string; readonly serverScope: string; readonly appInfo: EggAppInfo; + dirs?: EggDirInfo[]; /** * @class @@ -97,7 +103,7 @@ export class BaseLoader { * @see {@link AppInfo#pkg} * @since 1.0.0 */ - this.pkg = utility.readJSONSync(path.join(this.options.baseDir, 'package.json')); + this.pkg = readJSONSync(path.join(this.options.baseDir, 'package.json')); // auto require('tsconfig-paths/register') on typescript app // support env.EGG_TYPESCRIPT = true or { "egg": { "typescript": true } } on package.json @@ -224,7 +230,7 @@ export class BaseLoader { * @private * @since 1.0.0 */ - getAppname() { + getAppname(): string { if (this.pkg.name) { debug('Loaded appname(%s) from package.json', this.pkg.name); return this.pkg.name; @@ -238,7 +244,7 @@ export class BaseLoader { * @return {String} home directory * @since 3.4.0 */ - getHomedir() { + getHomedir(): string { // EGG_HOME for test return process.env.EGG_HOME || homedir() || '/home/admin'; } @@ -371,16 +377,16 @@ export class BaseLoader { * ``` * @since 1.0.0 */ - loadFile(filepath, ...inject) { - filepath = filepath && this.resolveModule(filepath); - if (!filepath) { + loadFile(filepath: string, ...inject: Array): any { + const realFilepath = filepath && this.resolveModule(filepath); + if (!realFilepath) { return null; } // function(arg1, args, ...) {} if (inject.length === 0) inject = [ this.app ]; - let ret = this.requireFile(filepath); + let ret = this.requireFile(realFilepath); if (is.function(ret) && !is.class(ret)) { ret = ret(...inject); } @@ -392,12 +398,12 @@ export class BaseLoader { * @return {Object} exports * @private */ - requireFile(filepath) { + requireFile(filepath: string): any { const timingKey = `Require(${this.#requiredCount++}) ${utils.getResolvedFilename(filepath, this.options.baseDir)}`; this.timing.start(timingKey); - const ret = utils.loadFile(filepath); + const moduleExports = utils.loadFile(filepath); this.timing.end(timingKey); - return ret; + return moduleExports; } /** @@ -415,16 +421,16 @@ export class BaseLoader { * @return {Array} loadUnits * @since 1.0.0 */ - getLoadUnits() { + getLoadUnits(): Array { if (this.dirs) { return this.dirs; } - const dirs = this.dirs = []; + this.dirs = []; if (this.orderPlugins) { for (const plugin of this.orderPlugins) { - dirs.push({ + this.dirs.push({ path: plugin.path, type: 'plugin', }); @@ -433,20 +439,20 @@ export class BaseLoader { // framework or egg path for (const eggPath of this.eggPaths) { - dirs.push({ + this.dirs.push({ path: eggPath, type: 'framework', }); } // application - dirs.push({ + this.dirs.push({ path: this.options.baseDir, type: 'app', }); - debug('Loaded dirs %j', dirs); - return dirs; + debug('Loaded dirs %j', this.dirs); + return this.dirs; } /** diff --git a/src/loader/egg_loader.ts b/src/loader/egg_loader.ts index 8b488cad..2ce47364 100644 --- a/src/loader/egg_loader.ts +++ b/src/loader/egg_loader.ts @@ -553,11 +553,11 @@ for (const loader of loaders) { Object.assign(EggLoader.prototype, loader); } -import { PluginLoader } from './mixin/plugin'; -import ConfigLoader from './mixin/config'; +import { PluginLoader } from './mixin/plugin.js'; +import ConfigLoader from './mixin/config.js'; // https://www.typescriptlang.org/docs/handbook/mixins.html#alternative-pattern -export interface EggLoader extends PluginLoader, ConfigLoader {} +export interface EggLoaderMixin extends PluginLoader, ConfigLoader {} // https://www.typescriptlang.org/docs/handbook/mixins.html function applyMixins(derivedCtor: any, constructors: any[]) { diff --git a/src/loader/file_loader.js b/src/loader/file_loader.ts similarity index 67% rename from src/loader/file_loader.js rename to src/loader/file_loader.ts index 9d734448..6f7d9b82 100644 --- a/src/loader/file_loader.js +++ b/src/loader/file_loader.ts @@ -1,35 +1,47 @@ -'use strict'; +import assert from 'node:assert'; +import fs from 'node:fs'; +import { debuglog } from 'node:util'; +import path from 'node:path'; +import globby from 'globby'; +import { isClass, isGeneratorFunction, isAsyncFunction, isPrimitive } from 'is-type-of'; +import utils from '../utils/index.js'; -const assert = require('assert'); -const fs = require('fs'); -const debug = require('node:util').debuglog('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 debug = debuglog('egg-core:loader'); -const defaults = { - directory: null, - target: null, - match: undefined, - ignore: undefined, - lowercaseFirst: false, - caseStyle: 'camel', - initializer: null, - call: true, - override: false, - inject: undefined, - filter: null, -}; +export const FULLPATH = Symbol('EGG_LOADER_ITEM_FULLPATH'); +export const EXPORTS = Symbol('EGG_LOADER_ITEM_EXPORTS'); + +export type CaseStyle = 'camel' | 'lower' | 'upper'; +export type CaseStyleFunction = (filepath: string) => string[]; +export type FileLoaderInitializer = (exports: unknown, options: { path: string; pathName: string }) => unknown; +export type FileLoaderFilter = (exports: unknown) => boolean; + +export interface FileLoaderOptions { + directory: string | string[]; + target: Record; + match?: string | string[]; + ignore?: string | string[]; + lowercaseFirst?: boolean; + caseStyle?: CaseStyle | CaseStyleFunction; + initializer?: FileLoaderInitializer; + call?: boolean; + override?: boolean; + inject?: object; + filter?: FileLoaderFilter; +} + +export interface FileLoaderParseItem { + fullpath: string; + properties: string[]; + exports: object | Function; +} /** * Load files from directory to target object. * @since 1.0.0 */ -class FileLoader { +export class FileLoader { + readonly options: FileLoaderOptions & Required>; /** * @class @@ -45,14 +57,20 @@ class FileLoader { * @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) { + constructor(options: FileLoaderOptions) { assert(options.directory, 'options.directory is required'); assert(options.target, 'options.target is required'); - this.options = Object.assign({}, defaults, options); + this.options = { + lowercaseFirst: false, + caseStyle: 'camel', + call: true, + override: false, + ...options, + }; // compatible old options _lowercaseFirst_ if (this.options.lowercaseFirst === true) { - deprecate('lowercaseFirst is deprecated, use caseStyle instead'); + console.warn('[egg-core:deprecated] lowercaseFirst is deprecated, use caseStyle instead'); this.options.caseStyle = 'lower'; } } @@ -63,8 +81,8 @@ class FileLoader { * @return {Object} target * @since 1.0.0 */ - load() { - const items = this.parse(); + async load(): Promise { + const items = await this.parse(); const target = this.options.target; for (const item of items) { debug('loading item %j', item); @@ -78,9 +96,9 @@ class FileLoader { 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; + if (obj && !isPrimitive(obj)) { + Reflect.set(obj, FULLPATH, item.fullpath); + Reflect.set(obj, EXPORTS, true); } } else { obj = target[property] || {}; @@ -119,7 +137,7 @@ class FileLoader { * @return {Array} items * @since 1.0.0 */ - parse() { + async parse(): Promise { let files = this.options.match; if (!files) { files = (process.env.EGG_TYPESCRIPT === 'true' && utils.extensions['.ts']) @@ -141,8 +159,8 @@ class FileLoader { directories = [ directories ]; } - const filter = is.function(this.options.filter) ? this.options.filter : null; - const items = []; + const filter = typeof this.options.filter === 'function' ? this.options.filter : null; + const items: FileLoaderParseItem[] = []; debug('parsing %j', directories); for (const directory of directories) { const filepaths = globby.sync(files, { cwd: directory }); @@ -151,17 +169,17 @@ class FileLoader { if (!fs.statSync(fullpath).isFile()) continue; // get properties // app/service/foo/bar.js => [ 'foo', 'bar' ] - const properties = getProperties(filepath, this.options); + const properties = getProperties(filepath, this.options.caseStyle); // 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); + const exports = await 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)) { + if (isClass(exports)) { exports.prototype.pathName = pathName; exports.prototype.fullPath = fullpath; } @@ -173,20 +191,15 @@ class FileLoader { 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 }) { +function getProperties(filepath: string, caseStyle: CaseStyle | CaseStyleFunction) { // if caseStyle is function, return the result of function - if (is.function(caseStyle)) { + if (typeof caseStyle === 'function') { const result = caseStyle(filepath); - assert(is.array(result), `caseStyle expect an array, but got ${result}`); + assert(Array.isArray(result), `caseStyle expect an array, but got ${JSON.stringify(result)}`); return result; } // use default camelize @@ -195,19 +208,23 @@ function getProperties(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); +async function getExports(fullpath: string, options: FileLoaderOptions, pathName: string) { + let exports = await utils.loadFile(fullpath); // process exports as you like - if (initializer) { - exports = initializer(exports, { path: fullpath, pathName }); + if (options.initializer) { + exports = options.initializer(exports, { path: fullpath, pathName }); + } + + if (isGeneratorFunction(exports)) { + throw new TypeError(`Support for generators was removed, fullpath: ${fullpath}`); } - // return exports when it's a class or generator + // return exports when it's a class or async function // // module.exports = class Service {}; // or - // module.exports = function*() {} - if (is.class(exports) || is.generatorFunction(exports) || is.asyncFunction(exports)) { + // module.exports = async function() {} + if (isClass(exports) || isAsyncFunction(exports)) { return exports; } @@ -216,8 +233,8 @@ function getExports(fullpath, { initializer, call, inject }, pathName) { // module.exports = function(app) { // return {}; // } - if (call && is.function(exports)) { - exports = exports(inject); + if (options.call && typeof exports === 'function') { + exports = exports(options.inject); if (exports != null) { return exports; } @@ -227,7 +244,7 @@ function getExports(fullpath, { initializer, call, inject }, pathName) { return exports; } -function defaultCamelize(filepath, caseStyle) { +function defaultCamelize(filepath: string, caseStyle: CaseStyle) { const properties = filepath.substring(0, filepath.lastIndexOf('.')).split('/'); return properties.map(property => { if (!/^[a-z][a-z0-9_-]*$/i.test(property)) { diff --git a/src/loader/mixin/config.js b/src/loader/mixin/config.ts similarity index 89% rename from src/loader/mixin/config.js rename to src/loader/mixin/config.ts index 49c3ad64..e0affd3c 100644 --- a/src/loader/mixin/config.js +++ b/src/loader/mixin/config.ts @@ -1,11 +1,12 @@ -'use strict'; +import { debuglog } from 'node:util'; +import path from 'node:path'; +import assert from 'node:assert'; +import { extend } from 'extend2'; +import { Timing } from '../../utils/timing.js'; -const debug = require('node:util').debuglog('egg-core:config'); -const path = require('path'); -const extend = require('extend2'); -const assert = require('assert'); +const debug = debuglog('egg-core:config'); -module.exports = { +export class EggConfigLoader { /** * Load config/config.js @@ -19,7 +20,7 @@ module.exports = { this.timing.start('Load Config'); this.configMeta = {}; - const target = {}; + const target: Record = {}; // Load Application config first const appConfig = this._preloadAppConfig(); @@ -108,14 +109,14 @@ module.exports = { } }, - _setConfigMeta(config, filepath) { + _setConfigMeta(config: Record, filepath: string) { config = extend(true, {}, config); setConfig(config, filepath); extend(true, this.configMeta, config); - }, -}; + } +} -function setConfig(obj, filepath) { +function setConfig(obj: Record, filepath: string) { for (const key of Object.keys(obj)) { const val = obj[key]; // ignore console diff --git a/src/loader/mixin/plugin.ts b/src/loader/mixin/plugin.ts index ce7bf09b..1b3c8b0b 100644 --- a/src/loader/mixin/plugin.ts +++ b/src/loader/mixin/plugin.ts @@ -2,11 +2,11 @@ import assert from 'node:assert'; import fs from 'node:fs'; import path from 'node:path'; import { debuglog } from 'node:util'; -import sequencify from '../../utils/sequencify'; -import utils from '../../utils'; +import sequencify from '../../utils/sequencify.js'; +import utils from '../../utils/index.js'; import { BaseLoader, PluginInfo } from '../base_loader'; -const debug = debuglog('@eggjs/core:loader:plugin'); +const debug = debuglog('egg-core:loader:plugin'); export class PluginLoader extends BaseLoader { protected lookupDirs: Set; diff --git a/src/utils/index.ts b/src/utils/index.ts index 517aa0b0..b0138169 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,14 +1,12 @@ import path from 'node:path'; import fs from 'node:fs'; import BuiltinModule from 'node:module'; -import convert from 'koa-convert'; -import is from 'is-type-of'; -import co from 'co'; +import { isGeneratorFunction } from 'is-type-of'; export type Fun = (...args: any[]) => any; // Guard against poorly mocked module constructors. -const Module = module.constructor.length > 1 +const Module = typeof module !== 'undefined' && module.constructor.length > 1 ? module.constructor /* istanbul ignore next */ : BuiltinModule; @@ -16,21 +14,35 @@ const Module = module.constructor.length > 1 export default { extensions: (Module as any)._extensions, - loadFile(filepath: string) { + async loadFile(filepath: string) { try { // if not js module, just return content buffer const extname = path.extname(filepath); if (extname && !(Module as any)._extensions[extname]) { return fs.readFileSync(filepath); } - // require js module - // eslint-disable-next-line @typescript-eslint/no-var-requires - const obj = require(filepath); + let obj: any; + let isESM = false; + if (typeof require === 'function') { + // commonjs + obj = require(filepath); + if (obj && obj.__esModule) { + isESM = true; + } + } else { + // esm + obj = await import(filepath); + isESM = true; + if (obj && obj.__esModule && 'default' in obj) { + // default: { default: [Function (anonymous)] } + obj = obj.default; + } + } if (!obj) return obj; - // it's es module - if (obj.__esModule) return 'default' in obj ? obj.default : obj; + // it's es module, use default export + if (isESM) return 'default' in obj ? obj.default : obj; return obj; - } catch (err) { + } catch (err: any) { err.message = `[@eggjs/core] load file: ${filepath}, error: ${err.message}`; throw err; } @@ -41,12 +53,17 @@ export default { async callFn(fn: Fun, args?: any[], ctx?: any) { args = args || []; if (typeof fn !== 'function') return; - if (is.generatorFunction(fn)) fn = co.wrap(fn); + if (isGeneratorFunction(fn)) { + throw new TypeError(`Support for generators was removed, function: ${fn.toString()}`); + } return ctx ? fn.call(ctx, ...args) : fn(...args); }, middleware(fn: any) { - return is.generatorFunction(fn) ? convert(fn) : fn; + if (isGeneratorFunction(fn)) { + throw new TypeError(`Support for generators was removed, middleware: ${fn.toString()}`); + } + return fn; }, getCalleeFromStack(withLine?: boolean, stackIndex?: number) { @@ -88,12 +105,10 @@ export default { }, }; - /** * Capture call site stack from v8. * https://github.com/v8/v8/wiki/Stack-Trace-API */ - -function prepareObjectStackTrace(_obj, stack) { +function prepareObjectStackTrace(_obj: any, stack: any) { return stack; } diff --git a/src/utils/sequencify.ts b/src/utils/sequencify.ts index d7358411..6bb7c0f6 100644 --- a/src/utils/sequencify.ts +++ b/src/utils/sequencify.ts @@ -2,35 +2,40 @@ import { debuglog } from 'node:util'; const debug = debuglog('@eggjs/core:utils:sequencify'); -function sequence(tasks, names, results, missing, recursive, nest, optional, parent) { +export interface SequencifyResult { + sequence: string[]; + requires: Record; +} + +function sequence(tasks, names: string[], result: SequencifyResult, missing: string[], recursive: string[], + nest: string[], optional: boolean, parent: string) { names.forEach(function(name) { - if (results.requires[name]) return; + if (result.requires[name]) return; const node = tasks[name]; - if (!node) { if (optional === true) return; missing.push(name); } else if (nest.includes(name)) { nest.push(name); - recursive.push(nest.slice(0)); - nest.pop(name); + recursive.push(...nest.slice(0)); + nest.pop(); } else if (node.dependencies.length || node.optionalDependencies.length) { nest.push(name); if (node.dependencies.length) { - sequence(tasks, node.dependencies, results, missing, recursive, nest, optional, name); + sequence(tasks, node.dependencies, result, missing, recursive, nest, optional, name); } if (node.optionalDependencies.length) { - sequence(tasks, node.optionalDependencies, results, missing, recursive, nest, true, name); + sequence(tasks, node.optionalDependencies, result, missing, recursive, nest, true, name); } - nest.pop(name); + nest.pop(); } if (!optional) { - results.requires[name] = true; + result.requires[name] = true; debug('task: %s is enabled by %s', name, parent); } - if (!results.sequence.includes(name)) { - results.sequence.push(name); + if (!result.sequence.includes(name)) { + result.sequence.push(name); } }); } @@ -38,21 +43,21 @@ function sequence(tasks, names, results, missing, recursive, nest, optional, par // tasks: object with keys as task names // names: array of task names export default function sequencify(tasks, names: string[]) { - const results = { + const result: SequencifyResult = { sequence: [], requires: {}, }; // the final sequence - const missing = []; // missing tasks - const recursive = []; // recursive task dependencies + const missing: string[] = []; // missing tasks + const recursive: string[] = []; // recursive task dependencies - sequence(tasks, names, results, missing, recursive, [], false, 'app'); + sequence(tasks, names, result, missing, recursive, [], false, 'app'); if (missing.length || recursive.length) { - results.sequence = []; // results are incomplete at best, completely wrong at worst, remove them to avoid confusion + result.sequence = []; // results are incomplete at best, completely wrong at worst, remove them to avoid confusion } return { - sequence: results.sequence.filter(item => results.requires[item]), + sequence: result.sequence.filter(item => result.requires[item]), missingTasks: missing, recursiveDependencies: recursive, }; diff --git a/test/loader/file_loader.test.js b/test/loader/file_loader.test.ts similarity index 53% rename from test/loader/file_loader.test.js rename to test/loader/file_loader.test.ts index 1091220d..1c9b71dd 100644 --- a/test/loader/file_loader.test.js +++ b/test/loader/file_loader.test.ts @@ -1,15 +1,16 @@ -const assert = require('assert'); -const pedding = require('pedding'); -const path = require('path'); -const is = require('is-type-of'); -const yaml = require('js-yaml'); -const FileLoader = require('../../lib/loader/file_loader'); -const dirBase = path.join(__dirname, '../fixtures/load_dirs'); - -describe('test/loader/file_loader.test.js', () => { - it('should load files', done => { - const services = {}; - new FileLoader({ +import { strict as assert } from 'node:assert'; +import path from 'node:path'; +import { isClass } from 'is-type-of'; +import yaml from 'js-yaml'; +import { FileLoader } from '../../src/loader/file_loader.js'; +import { getFilepath } from '../utils.js'; + +const dirBase = getFilepath('load_dirs'); + +describe('test/loader/file_loader.test.ts', () => { + it('should load files', async () => { + const services: Record = {}; + await new FileLoader({ directory: path.join(dirBase, 'services'), target: services, }).load(); @@ -22,33 +23,37 @@ describe('test/loader/file_loader.test.js', () => { assert(services.hyphenDir.a); assert(services.underscoreDir.a); assert(services.userProfile); - - done = pedding(2, done); - services.foo.get((err, v) => { - assert.ifError(err); - assert(v === 'bar'); - done(); - }); - services.userProfile.getByName('mk2', (err, user) => { - assert.ifError(err); - assert.deepEqual(user, { name: 'mk2' }); - done(); - }); - assert('load' in services.dir.service); assert('app' in services.dir.service); - assert(services.dir.service.load === true); + assert.equal(services.dir.service.load, true); + + await Promise.all([ + new Promise(resolve => { + services.foo.get((err: Error, v: string) => { + assert.ifError(err); + assert.equal(v, 'bar'); + resolve(); + }); + }), + new Promise(resolve => { + services.userProfile.getByName('mk2', (err: Error, user: object) => { + assert.ifError(err); + assert.deepEqual(user, { name: 'mk2' }); + resolve(); + }); + }), + ]); }); - it('should not overwrite property', () => { + it('should not overwrite property', async () => { const app = { services: { foo: {}, }, }; - assert.throws( - () => { - new FileLoader({ + await assert.rejects( + async () => { + await new FileLoader({ directory: path.join(dirBase, 'services'), target: app.services, }).load(); @@ -57,23 +62,22 @@ describe('test/loader/file_loader.test.js', () => { ); }); - it('should not overwrite property from loading', () => { - const app = { services: {} }; - assert.throws(() => { - new FileLoader({ + it('should not overwrite property from loading', async () => { + const app: Record = { services: {} }; + await assert.rejects(async () => { + await new FileLoader({ directory: [ path.join(dirBase, 'services'), path.join(dirBase, 'overwrite_services'), ], target: app.services, - logger: console, }).load(); }, /can't overwrite property 'foo'/); }); - it('should overwrite property from loading', () => { + it('should overwrite property from loading', async () => { const app = { services: {} }; - new FileLoader({ + await new FileLoader({ directory: [ path.join(dirBase, 'services'), path.join(dirBase, 'overwrite_services'), @@ -83,9 +87,9 @@ describe('test/loader/file_loader.test.js', () => { }).load(); }); - it('should loading without call function', () => { - const app = { services: {} }; - new FileLoader({ + it('should loading without call function', async () => { + const app: Record = { services: {} }; + await new FileLoader({ directory: path.join(dirBase, 'services'), target: app.services, call: false, @@ -93,9 +97,9 @@ describe('test/loader/file_loader.test.js', () => { assert.deepEqual(app.services.fooService(), { a: 1 }); }); - it('should loading without call es6 class', () => { - const app = { services: {} }; - new FileLoader({ + it('should loading without call es6 class', async () => { + const app: Record = { services: {} }; + await new FileLoader({ directory: path.join(dirBase, 'class'), target: app.services, }).load(); @@ -106,9 +110,9 @@ describe('test/loader/file_loader.test.js', () => { assert.deepEqual(instance.getUser(), { name: 'xiaochen.gaoxc' }); }); - it('should loading without call babel class', () => { - const app = { services: {} }; - new FileLoader({ + it('should loading without call babel class', async () => { + const app: Record = { services: {} }; + await new FileLoader({ directory: path.join(dirBase, 'babel'), target: app.services, }).load(); @@ -116,16 +120,16 @@ describe('test/loader/file_loader.test.js', () => { assert.deepEqual(instance.getUser(), { name: 'xiaochen.gaoxc' }); }); - it('should only load property match the filers', () => { - const app = { middlewares: {} }; - new FileLoader({ + it('should only load property match the filers', async () => { + const app: Record = { middlewares: {} }; + await new FileLoader({ directory: [ path.join(dirBase, 'middlewares/default'), path.join(dirBase, 'middlewares/app'), ], target: app.middlewares, call: false, - filters: [ 'm1', 'm2', 'dm1', 'dm2' ], + // filters: [ 'm1', 'm2', 'dm1', 'dm2' ], }).load(); assert(app.middlewares.m1); assert(app.middlewares.m2); @@ -133,29 +137,29 @@ describe('test/loader/file_loader.test.js', () => { assert(app.middlewares.dm2); }); - it('should support ignore string', () => { - const app = { services: {} }; - new FileLoader({ + it('should support ignore string', async () => { + const app: Record = { services: {} }; + await new FileLoader({ directory: path.join(dirBase, 'ignore'), target: app.services, ignore: 'util/**', }).load(); - assert.deepEqual(app.services.a, { a: 1 }); + assert.equal(app.services.a.a, 1); }); - it('should support ignore array', () => { - const app = { services: {} }; - new FileLoader({ + it('should support ignore array', async () => { + const app: Record = { services: {} }; + await new FileLoader({ directory: path.join(dirBase, 'ignore'), target: app.services, ignore: [ 'util/a.js', 'util/b/b.js' ], }).load(); - assert.deepEqual(app.services.a, { a: 1 }); + assert.equal(app.services.a.a, 1); }); - it('should support lowercase first letter', () => { - const app = { services: {} }; - new FileLoader({ + it('should support lowercase first letter', async () => { + const app: Record = { services: {} }; + await new FileLoader({ directory: path.join(dirBase, 'lowercase'), target: app.services, lowercaseFirst: true, @@ -165,87 +169,87 @@ describe('test/loader/file_loader.test.js', () => { assert(app.services.someDir.someSubClass); }); - it('should support options.initializer with es6 class', () => { - const app = { dao: {} }; - new FileLoader({ + it('should support options.initializer with es6 class', async () => { + const app: Record = { dao: {} }; + await new FileLoader({ directory: path.join(dirBase, 'dao'), target: app.dao, ignore: 'util/**', - initializer(exports, opt) { + initializer(exports: any, opt) { return new exports(app, opt.path); }, }).load(); assert(app.dao.TestClass); assert.deepEqual(app.dao.TestClass.user, { name: 'kai.fangk' }); - assert(app.dao.TestClass.app === app); - assert(app.dao.TestClass.path === path.join(dirBase, 'dao/TestClass.js')); - assert.deepEqual(app.dao.testFunction, { user: { name: 'kai.fangk' } }); - assert.deepEqual(app.dao.testReturnFunction, { user: { name: 'kai.fangk' } }); + assert.equal(app.dao.TestClass.app, app); + assert.equal(app.dao.TestClass.path, path.join(dirBase, 'dao/TestClass.js')); + assert.deepEqual(app.dao.testFunction.user, { name: 'kai.fangk' }); + assert.deepEqual(app.dao.testReturnFunction.user, { name: 'kai.fangk' }); }); - it('should support options.initializer custom type', () => { - const app = { yml: {} }; - new FileLoader({ + it('should support options.initializer custom type', async () => { + const app: Record = { yml: {} }; + await new FileLoader({ directory: path.join(dirBase, 'yml'), match: '**/*.yml', target: app.yml, - initializer(exports) { + initializer(exports: any) { return yaml.load(exports.toString()); }, }).load(); assert(app.yml.config); - assert.deepEqual(app.yml.config, { map: { a: 1, b: 2 } }); + assert.deepEqual(app.yml.config.map, { a: 1, b: 2 }); }); - it('should pass es6 module', () => { - const app = { model: {} }; - new FileLoader({ + it('should pass es6 module', async () => { + const app: Record = { model: {} }; + await new FileLoader({ directory: path.join(dirBase, 'es6_module'), target: app.model, }).load(); - assert.deepEqual(app.model.mod, { a: 1 }); + assert.deepEqual(app.model.mod.a, 1); }); - it('should contain syntax error filepath', () => { - const app = { model: {} }; - assert.throws(() => { - new FileLoader({ + it('should contain syntax error filepath', async () => { + const app: Record = { model: {} }; + await assert.rejects(async () => { + await new FileLoader({ directory: path.join(dirBase, 'syntax_error'), target: app.model, }).load(); }, /error: Unexpected identifier/); }); - it('should throw when directory contains dot', () => { + it('should throw when directory contains dot', async () => { const mod = {}; - assert.throws(() => { - new FileLoader({ + await assert.rejects(async () => { + await new FileLoader({ directory: path.join(dirBase, 'error/dotdir'), target: mod, }).load(); }, /dot.dir is not match 'a-z0-9_-' in dot.dir\/a.js/); }); - it('should throw when directory contains underscore', () => { - const mod = {}; - assert.throws(() => { - new FileLoader({ + it('should throw when directory contains underscore', async () => { + const mod: Record = {}; + await assert.rejects(async () => { + await new FileLoader({ directory: path.join(dirBase, 'error/underscore-dir'), target: mod, }).load(); }, /_underscore is not match 'a-z0-9_-' in _underscore\/a.js/); - assert.throws(() => { - new FileLoader({ + await assert.rejects(async () => { + await new FileLoader({ directory: path.join(dirBase, 'error/underscore-file-in-dir'), target: mod, }).load(); }, /_a is not match 'a-z0-9_-' in dir\/_a.js/); }); - it('should throw when file starts with underscore', () => { - const mod = {}; - assert.throws(() => { - new FileLoader({ + it('should throw when file starts with underscore', async () => { + const mod: Record = {}; + await assert.rejects(async () => { + await new FileLoader({ directory: path.join(dirBase, 'error/underscore-file'), target: mod, }).load(); @@ -253,9 +257,9 @@ describe('test/loader/file_loader.test.js', () => { }); describe('caseStyle', () => { - it('should load when caseStyle = upper', () => { - const target = {}; - new FileLoader({ + it('should load when caseStyle = upper', async () => { + const target: Record = {}; + await new FileLoader({ directory: path.join(dirBase, 'camelize'), target, caseStyle: 'upper', @@ -267,9 +271,9 @@ describe('test/loader/file_loader.test.js', () => { assert(target.FooBar4); }); - it('should load when caseStyle = camel', () => { - const target = {}; - new FileLoader({ + it('should load when caseStyle = camel', async () => { + const target: Record = {}; + await new FileLoader({ directory: path.join(dirBase, 'camelize'), target, caseStyle: 'camel', @@ -281,9 +285,9 @@ describe('test/loader/file_loader.test.js', () => { assert(target.fooBar4); }); - it('should load when caseStyle = lower', () => { - const target = {}; - new FileLoader({ + it('should load when caseStyle = lower', async () => { + const target: Record = {}; + await new FileLoader({ directory: path.join(dirBase, 'camelize'), target, caseStyle: 'lower', @@ -295,9 +299,9 @@ describe('test/loader/file_loader.test.js', () => { assert(target.fooBar4); }); - it('should load when caseStyle is function', () => { - const target = {}; - new FileLoader({ + it('should load when caseStyle is function', async () => { + const target: Record = {}; + await new FileLoader({ directory: path.join(dirBase, 'camelize'), target, caseStyle(filepath) { @@ -314,22 +318,22 @@ describe('test/loader/file_loader.test.js', () => { assert(target['foo-bar4']); }); - it('should throw when caseStyle do not return array', () => { - const target = {}; - assert.throws(() => { - new FileLoader({ + it('should throw when caseStyle do not return array', async () => { + const target: Record = {}; + await assert.rejects(async () => { + await new FileLoader({ directory: path.join(dirBase, 'camelize'), target, - caseStyle(filepath) { - return filepath; + caseStyle(filepath: string) { + return filepath as any; }, }).load(); }, /caseStyle expect an array, but got/); }); - it('should be overridden by lowercaseFirst', () => { - const target = {}; - new FileLoader({ + it('should be overridden by lowercaseFirst', async () => { + const target: Record = {}; + await new FileLoader({ directory: path.join(dirBase, 'camelize'), target, caseStyle: 'upper', @@ -343,24 +347,24 @@ describe('test/loader/file_loader.test.js', () => { }); }); - it('should load files with inject', () => { - const inject = {}; - const target = {}; - new FileLoader({ + it('should load files with inject', async () => { + const inject: Record = {}; + const target: Record = {}; + await new FileLoader({ directory: path.join(dirBase, 'inject'), target, inject, }).load(); - assert(inject.b === true); + assert.equal(inject.b, true); new target.a(inject); - assert(inject.a === true); + assert.equal(inject.a, true); }); - it('should load files with filter', () => { - const target = {}; - new FileLoader({ + it('should load files with filter', async () => { + const target: Record = {}; + await new FileLoader({ directory: path.join(dirBase, 'filter'), target, filter(obj) { @@ -369,11 +373,11 @@ describe('test/loader/file_loader.test.js', () => { }).load(); assert.deepEqual(Object.keys(target), [ 'arr' ]); - new FileLoader({ + await new FileLoader({ directory: path.join(dirBase, 'filter'), target, filter(obj) { - return is.class(obj); + return isClass(obj); }, }).load(); assert.deepEqual(Object.keys(target), [ 'arr', 'class' ]); diff --git a/test/utils.ts b/test/utils.ts index a46c38e8..518e4a86 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,23 +1,27 @@ import path from 'node:path'; -import { Application as EggApplication } from './fixtures/egg'; +import { fileURLToPath } from 'node:url'; +// import { Application as EggApplication } from './fixtures/egg'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); export function getFilepath(name: string) { return path.join(__dirname, 'fixtures', name); } -export function createApp(name, options) { - const baseDir = this.getFilepath(name); - options = options || {}; - options.baseDir = baseDir; - options.type = options.type || 'application'; +// export function createApp(name: string, options: any) { +// const baseDir = getFilepath(name); +// options = options || {}; +// options.baseDir = baseDir; +// options.type = options.type || 'application'; - let CustomApplication = EggApplication; - if (options.Application) { - CustomApplication = options.Application; - } +// let CustomApplication = EggApplication; +// if (options.Application) { +// CustomApplication = options.Application; +// } - return new CustomApplication(options); -} +// return new CustomApplication(options); +// } export const symbol = { view: Symbol('view'), diff --git a/tsconfig.json b/tsconfig.json index 1e5143c6..ff41b734 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,10 @@ { "extends": "@eggjs/tsconfig", "compilerOptions": { + "strict": true, + "noImplicitAny": true, "target": "ES2022", - "module": "Node16", - "outDir": "lib", - "useUnknownInCatchVariables": false - }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "test" - ] + "module": "NodeNext", + "moduleResolution": "NodeNext" + } }