Match-id-2ee2e4235032ec1ddfc09c904eae3c6341d20ee2

This commit is contained in:
* 2023-09-18 15:22:25 +08:00
parent 9be2395a60
commit 331f8a91c4
99 changed files with 3444 additions and 0 deletions

View File

@ -0,0 +1,40 @@
module.exports = {
'parser': 'babel-eslint',
'env': {
'amd': true,
'es6': true,
'browser': true,
'node': false
},
'parserOptions': {
'ecmaVersion': 6,
'sourceType': 'module',
'ecmaFeatures': {
'jsx': true
}
},
'ignorePatterns': [
"src/template"
],
'rules': {
'indent': [
'error',
4,
{
SwitchCase: 1,
flatTernaryExpressions: true
}
],
'no-unused-vars': 'off', // 允许变量声明后未使用
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
"no-underscore-dangle": ["off", "always"], // 允许私有变量 _xxx的变量命名方式
'filenames/match-exported': 0,
'consistent-return': 0,
"comma-dangle": [2, "never"], // 组和对象键值对最后一个逗号, never参数不能带末尾的逗号, always参数必须带末尾的逗号
'global-require': 0, // 允许require语句不出现在顶层中
'no-nested-ternary': 0, // 允许嵌套三元表达式
'no-unused-expressions': 0, // 允许使用未执行的表达式。比如fn是一个函数允许 fn && fn()
'no-throw-literal': 0, // 允许throw抛出对象格式
'@typescript-eslint/member-ordering': 0 // 禁用TypeScript声明规范
}
}

View File

@ -0,0 +1,3 @@
node_modules/
webpack/
public/

View File

@ -0,0 +1,15 @@
export default {
printWidth: 120, // 一行120字符数如果超过会进行换行
tabWidth: 2, // tab等2个空格
useTabs: false, // 用空格缩进行
semi: true, // 行尾使用分号
singleQuote: true, // 字符串使用单引号
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
jsxSingleQuote: false, // 在JSX中使用双引号
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
bracketSpacing: true, // 对象的括号间增加空格
jsxBracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
endOfLine: 'lf', // 仅限换行(\n
};

View File

@ -0,0 +1,5 @@
#!/usr/bin/env node
import run from '../lib/cli/cli.js';
run();

View File

@ -0,0 +1,2 @@
declare const _default: (api: any) => void;
export default _default;

View File

@ -0,0 +1,49 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import webpack from 'webpack';
import { build } from 'vite';
export default (api) => {
api.registerCommand({
name: 'build',
description: 'build application for production',
initialState: api.buildConfig,
fn: function (args, state) {
return __awaiter(this, void 0, void 0, function* () {
switch (api.compileMode) {
case 'webpack':
if (state) {
api.applyHook({ name: 'beforeCompile', args: state });
state.forEach((s) => {
webpack(s.config, (err, stats) => {
// api.applyHook({ name: 'afterCompile' });
if (err || stats.hasErrors()) {
api.logger.error(`Build failed.err: ${err}, stats:${stats}`);
}
});
});
}
else {
api.logger.error(`Build failed. Can't find build config.`);
}
break;
case 'vite':
if (state) {
api.applyHook({ name: 'beforeCompile' });
build(state);
}
else {
api.logger.error(`Build failed. Can't find build config.`);
}
break;
}
});
},
});
};

View File

@ -0,0 +1,3 @@
import { API } from '../../../types/types';
declare const _default: (api: API) => void;
export default _default;

View File

@ -0,0 +1,72 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import { createServer } from 'vite';
import setupProxy from '../../../utils/setupProxy.js';
export default (api) => {
api.registerCommand({
name: 'dev',
description: 'build application for development',
initialState: api.devBuildConfig,
fn: function (args, state) {
return __awaiter(this, void 0, void 0, function* () {
api.applyHook({ name: 'beforeDevConfig' });
switch (api.compileMode) {
case 'webpack':
if (state) {
api.applyHook({ name: 'beforeDevCompile', config: state });
const compiler = webpack(state);
const devServerOptions = {
client: {
overlay: false,
},
host: 'localhost',
port: '8888',
open: true,
historyApiFallback: true,
};
if (api.userConfig.devBuildConfig.devProxy) {
devServerOptions.onBeforeSetupMiddleware = (devServer) => {
setupProxy(devServer.app, api);
};
}
api.applyHook({
name: 'beforeStartDevServer',
config: { compiler: compiler, devServerOptions: devServerOptions },
});
const server = new WebpackDevServer(compiler, devServerOptions);
server.startCallback((err) => {
api.applyHook({ name: 'afterStartDevServer' });
});
}
else {
api.logger.error("Can't find config");
}
break;
case 'vite':
if (state) {
yield createServer(state)
.then(server => {
return server.listen();
})
.then(server => {
server.printUrls();
});
}
else {
api.logger.error("Can't find config");
}
break;
}
});
},
});
};

View File

@ -0,0 +1,3 @@
import { API } from '../../../types/types';
declare const _default: (api: API) => void;
export default _default;

View File

@ -0,0 +1,100 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import inquirer from 'inquirer';
import fs from 'fs';
import path from 'path';
import { copyFile } from '../../../utils/util.js';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default (api) => {
api.registerCommand({
name: 'generate',
description: 'generate template',
fn: (args) => __awaiter(void 0, void 0, void 0, function* () {
if (args._[0] === 'g') {
args._.shift();
}
if (args._.length === 0) {
api.logger.warn("Can't find any generate options.");
return;
}
switch (args._[0]) {
case 'jest':
args._.shift();
const isESM = api.packageJson['type'] === 'module';
yield generateJest(args, api.cwd, isESM);
}
}),
});
};
const generateJest = (args, cwd, isESM) => __awaiter(void 0, void 0, void 0, function* () {
let isTs = false;
if (args['ts']) {
isTs = true;
}
else {
const answers = yield inquirer.prompt([
{
name: 'useTs',
message: 'Do you want to use TypeScript',
type: 'confirm',
},
]);
isTs = answers['useTs'];
}
if (checkJestConfigExist(cwd)) {
console.log('The jest config is exist.');
return;
}
const testRootPath = path.join(cwd, 'test');
if (!fs.existsSync(testRootPath)) {
fs.mkdirSync(testRootPath);
}
let templateDir = path.resolve(__dirname, '../../../../template/test');
// 如果是TS, 拷贝ts
if (isTs) {
templateDir = path.join(templateDir, 'ts');
copyTestTemplate(cwd, testRootPath, templateDir);
}
// 拷贝mjs
if (!isTs && isESM) {
templateDir = path.join(templateDir, 'mjs');
copyTestTemplate(cwd, testRootPath, templateDir);
}
// 拷贝cjs
if (!isTs && !isESM) {
templateDir = path.join(templateDir, 'cjs');
copyTestTemplate(cwd, testRootPath, templateDir);
}
});
function checkJestConfigExist(cwd) {
const items = fs.readdirSync(cwd);
for (const item of items) {
const itemPath = path.resolve(cwd, item);
const states = fs.statSync(itemPath);
if (states.isFile() && item.startsWith('jest.config')) {
return true;
}
}
return false;
}
function copyTestTemplate(cwd, testRootPath, templateDir) {
const items = fs.readdirSync(templateDir);
for (const item of items) {
const itemPath = path.resolve(templateDir, item);
if (item.startsWith('jest.config')) {
copyFile(path.join(cwd, item), itemPath);
}
else {
copyFile(path.join(testRootPath, item), itemPath);
}
}
}

View File

@ -0,0 +1,2 @@
declare const _default: (api: any) => void;
export default _default;

View File

@ -0,0 +1,31 @@
import chalk from 'chalk';
import lodash from 'lodash';
function getDescriptions(commands) {
return Object.keys(commands)
.filter(name => typeof commands[name] !== 'string')
.map(name => {
return getDescription(commands[name]);
});
}
function getDescription(command) {
return ` ${chalk.green(lodash.padEnd(command.name, 10))}${command.description || ''}`;
}
function padLeft(str) {
return str
.split('\n')
.map((line) => ` ${line}`)
.join('\n');
}
export default (api) => {
api.registerCommand({
name: 'help',
description: 'show command helps',
fn: (args, config) => {
console.log(`
Usage: inula-cli <command> [options]
${getDescriptions(api.commands).join('\n')}
`);
},
});
};

View File

@ -0,0 +1,2 @@
declare const _default: (api: any) => void;
export default _default;

View File

@ -0,0 +1,24 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { createRequire } from 'module';
import mockServer from '../../../utils/mockServer.js';
const require = createRequire(import.meta.url);
export default (api) => {
api.registerHook({
name: 'beforeStartDevServer',
fn: (state) => __awaiter(void 0, void 0, void 0, function* () {
const { compiler, devServerOptions } = state;
devServerOptions.setupMiddlewares = (middlewares, devServer) => {
mockServer(devServer.app);
return middlewares;
};
}),
});
};

View File

@ -0,0 +1,2 @@
declare const _default: (api: any) => void;
export default _default;

View File

@ -0,0 +1,53 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import express from 'express';
import { createRequire } from 'module';
import { createProxyMiddleware } from 'http-proxy-middleware';
const require = createRequire(import.meta.url);
export default (api) => {
api.registerCommand({
name: 'proxy',
description: 'remote proxy',
initialState: api.userConfig.remoteProxy,
fn: function (args, state) {
return __awaiter(this, void 0, void 0, function* () {
if (!state) {
api.logger.error(`Invalid proxy config!`);
return;
}
const app = express();
const proxyConfig = state;
const staticList = proxyConfig.localStatic;
staticList.forEach(function (value) {
app.use(value.url, express.static(value.local));
});
const remoteProxy = createProxyMiddleware(proxyConfig.fowardingURL, {
target: proxyConfig.target,
secure: false,
autoRewrite: true,
protocolRewrite: 'http',
ws: true,
hostRewrite: '',
preserveHeaderKeyCase: true,
proxyTimeout: 5 * 60 * 60 * 1000,
timeout: 5 * 60 * 60 * 1000,
onError: handleProxyError,
});
function handleProxyError(err) {
api.logger.error('Local proxy error. Error is ', err);
}
app.use(remoteProxy);
app.listen(proxyConfig.localPort, () => {
api.logger.info(`Start proxy client on http://localhost:${proxyConfig.localPort}`);
});
});
},
});
};

View File

@ -0,0 +1,3 @@
import { API } from '../../../types/types';
declare const _default: (api: API) => void;
export default _default;

View File

@ -0,0 +1,19 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import jest from 'jest';
export default (api) => {
api.registerCommand({
name: 'jest',
description: 'run jest test',
fn: (args, config) => __awaiter(void 0, void 0, void 0, function* () {
yield jest.run();
}),
});
};

View File

@ -0,0 +1,3 @@
import { API } from '../../../types/types';
declare const _default: (api: API) => void;
export default _default;

View File

@ -0,0 +1,21 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const pkgPath = path.resolve(__dirname, '../../../../package.json');
// 读取 package.json 文件
const packageJson = fs.readFileSync(pkgPath, 'utf8');
// 解析 JSON 格式的数据
const packageData = JSON.parse(packageJson);
// 获取版本号
const version = packageData.version;
export default (api) => {
api.registerCommand({
name: 'version',
description: 'show inula-cli version',
fn: () => {
api.logger.info(`Inula-cli version is ${version}.`);
},
});
};

1
packages/inula-cli/lib/cli/cli.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export default function run(): Promise<void>;

View File

@ -0,0 +1,74 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import chalk from 'chalk';
import yargsParser from 'yargs-parser';
import Hub from '../core/Hub.js';
import initializeEnv from '../utils/initializeEnv.js';
import { Logger, LogLevel } from '../utils/logger.js';
export default function run() {
return __awaiter(this, void 0, void 0, function* () {
const args = yargsParser(process.argv.slice(2));
const alias = {
h: 'help',
v: 'version',
g: 'generate',
};
let command = args._[0];
if (!command) {
if (args['v'] || args['version']) {
command = 'v';
}
if (args['h'] || args['help']) {
command = 'h';
}
}
const aliasCommand = alias[command];
if (aliasCommand) {
command = aliasCommand;
}
initializeEnv();
if (command === 'version' || command === 'help') {
process.env.INNER_COMMAND = "true";
}
switch (command) {
case 'build':
process.env.NODE_ENV = 'production';
break;
case 'dev':
process.env.NODE_ENV = 'development';
break;
default:
process.env.NODE_ENV = 'development';
break;
}
let enableDebug = false;
if (process.env.DEBUG === "true") {
enableDebug = true;
}
const logger = new Logger(enableDebug ? LogLevel.DEBUG : LogLevel.INFO);
try {
new Hub({
logger: logger,
}).run({
command,
args,
});
}
catch (err) {
if (err instanceof Error) {
logger.error(chalk.red(err.message));
if (err.stack) {
logger.error(err.stack);
}
process.exit(1);
}
}
});
}

View File

@ -0,0 +1,20 @@
import { Logger } from '../utils/logger.js';
import { UserConfig } from '../types/types.js';
interface ConfigOpts {
cwd: string;
isLocal?: boolean;
logger: Logger;
}
export default class Config {
cwd: string;
isLocal: boolean;
configFile?: string | null;
logger: Logger;
constructor(opts: ConfigOpts);
getUserConfig(): Promise<UserConfig>;
getConfigFile(): string | null;
addModePath(file: string, mode: string): string;
requireConfigs(configFiles: string[]): Promise<UserConfig[]>;
mergeConfig(...configs: UserConfig[]): UserConfig;
}
export {};

View File

@ -0,0 +1,96 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { existsSync } from 'fs';
import { extname, join } from 'path';
import { parseRequireDeps, cleanRequireCache } from '../utils/util.js';
import deepmerge from 'deepmerge';
import { loadModule } from '../utils/loadModule.js';
const DEFAULT_CONFIG_FILES = ['.inula.ts', '.inula.js'];
export default class Config {
constructor(opts) {
this.cwd = opts.cwd || process.cwd();
this.isLocal = opts.isLocal || process.env.NODE_ENV === 'development';
this.logger = opts.logger;
}
getUserConfig() {
return __awaiter(this, void 0, void 0, function* () {
const configFile = this.getConfigFile();
if (configFile === null) {
this.logger.warn(`Can't find .inula.ts or .inula.js in ${this.cwd}`);
return {};
}
this.configFile = configFile;
if (configFile) {
let envConfigFile = undefined;
if (process.env.RUNNING_MODE) {
envConfigFile = this.addModePath(configFile, process.env.RUNNING_MODE);
}
// 配置文件的来源
// 1、默认的configFile 如.inula.ts
// 2、带环境变量的configFile 如.inula.cloud.ts
// 3、dev模式 包含local 如.inula.local.ts
const files = [configFile];
if (envConfigFile && existsSync(envConfigFile)) {
files.push(envConfigFile);
}
if (this.isLocal) {
const localConfigFile = this.addModePath(configFile, 'local');
if (existsSync(localConfigFile)) {
files.push(localConfigFile);
}
}
this.logger.debug(`Find user config files ${files}`);
// 依次加载配置文件中的依赖并刷新require中的缓存
const requireDeps = files.reduce((deps, file) => {
deps = deps.concat(parseRequireDeps(file));
return deps;
}, []);
requireDeps.forEach(cleanRequireCache);
const configs = yield this.requireConfigs(files);
return this.mergeConfig(...configs);
}
else {
return {};
}
});
}
getConfigFile() {
const configFileList = DEFAULT_CONFIG_FILES.map(f => join(this.cwd, f));
for (let configFile of configFileList) {
if (existsSync(configFile)) {
return configFile;
}
}
return null;
}
addModePath(file, mode) {
const ext = extname(file);
return file.replace(new RegExp(`${ext}$`), `.${mode}${ext}`);
}
requireConfigs(configFiles) {
return __awaiter(this, void 0, void 0, function* () {
const configs = [];
for (const file in configFiles) {
const content = yield loadModule(configFiles[file]);
if (content) {
configs.push(content);
}
}
return configs;
});
}
mergeConfig(...configs) {
let ret = {};
for (const config of configs) {
ret = deepmerge(ret, config);
}
return ret;
}
}

49
packages/inula-cli/lib/core/Hub.d.ts vendored Normal file
View File

@ -0,0 +1,49 @@
import Config from '../config/Config.js';
import { BuildConfig, DevProxy, UserConfig } from '../types/types.js';
import { ServiceStage } from '../enum/enum.js';
import Plugin from '../plugin/Plugin.js';
import { Logger } from '../utils/logger.js';
import yargsParser from 'yargs-parser';
import { PackageJSON } from 'resolve';
interface HubOpts {
cwd?: string;
logger?: Logger;
}
export default class Hub {
args: any;
cwd: string;
env: string | undefined;
configManager: Config;
userConfig: UserConfig;
packageJson: PackageJSON;
stage: ServiceStage;
buildConfig: {
name: string;
config: object;
}[];
pluginManager: Plugin;
buildConfigPath: BuildConfig[];
devBuildConfig: object;
compileMode: string;
builtInPlugins: string[];
pluginPaths: string[];
devProxy: DevProxy | null;
logger: Logger;
[key: string]: any;
constructor(opts: HubOpts);
setStage(stage: ServiceStage): void;
init(): Promise<void>;
getBuiltInPlugins(): string[];
run({ command, args }: {
command: string | number;
args: yargsParser.Arguments;
}): Promise<void>;
runCommand({ command, args }: {
command: string | number;
args: yargsParser.Arguments;
}): Promise<void>;
setCompileMode(): void;
analyzeBuildConfig(): Promise<void>;
getConfigName(name: string): string;
}
export {};

View File

@ -0,0 +1,188 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { join, isAbsolute } from 'path';
import Config from '../config/Config.js';
import { ServiceStage } from '../enum/enum.js';
import Plugin from '../plugin/Plugin.js';
import { existsSync } from 'fs';
import { createRequire } from 'module';
import { Logger, LogLevel } from '../utils/logger.js';
import { loadModule } from '../utils/loadModule.js';
import readDirectory from '../utils/readDirectory.js';
import path from 'path';
import { fileURLToPath } from 'url';
import { loadPkg } from '../utils/loadPkg.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
export default class Hub {
constructor(opts) {
this.userConfig = {};
this.stage = ServiceStage.uninitialized;
this.buildConfig = [];
this.buildConfigPath = [];
this.devBuildConfig = {};
this.compileMode = '';
this.builtInPlugins = [];
this.pluginPaths = [];
this.devProxy = null;
this.setStage(ServiceStage.constructor);
this.cwd = opts.cwd || process.cwd();
this.env = process.env.NODE_ENV;
if (!opts.logger) {
this.logger = new Logger(LogLevel.INFO);
}
else {
this.logger = opts.logger;
}
this.packageJson = loadPkg(path.join(this.cwd, './package.json'));
this.configManager = new Config({
cwd: this.cwd,
isLocal: this.env === 'development',
logger: this.logger,
});
this.pluginManager = new Plugin({
cwd: this.cwd,
hub: this,
logger: this.logger,
});
}
setStage(stage) {
this.stage = stage;
}
init() {
return __awaiter(this, void 0, void 0, function* () {
this.setStage(ServiceStage.init);
// 获取用户配置
this.userConfig = yield this.configManager.getUserConfig();
// 设置编译模式
this.setCompileMode();
// 获取编译配置
yield this.analyzeBuildConfig();
this.setStage(ServiceStage.initPlugins);
this.builtInPlugins = this.getBuiltInPlugins();
yield this.pluginManager.register(this.builtInPlugins, this.userConfig.plugins);
this.setStage(ServiceStage.initHooks);
this.pluginManager.initHook();
});
}
getBuiltInPlugins() {
return readDirectory(path.resolve(__dirname, '../builtInPlugins'));
}
run({ command, args }) {
return __awaiter(this, void 0, void 0, function* () {
args._ = args._ || [];
if (args._[0] === command) {
args._.shift();
}
this.args = args;
yield this.init();
this.setStage(ServiceStage.run);
return this.runCommand({ command, args });
});
}
runCommand({ command, args }) {
return __awaiter(this, void 0, void 0, function* () {
this.logger.debug(`run command ${command}`);
const commands = typeof this.pluginManager.commands[command] === 'string'
? this.pluginManager.commands[this.pluginManager.commands[command]]
: this.pluginManager.commands[command];
if (commands === undefined) {
this.logger.error(`Invalid command ${command}`);
return;
}
const { fn } = commands;
return fn(args, this.pluginManager.store[command]);
});
}
setCompileMode() {
this.compileMode = this.userConfig.compileMode || 'webpack';
this.logger.debug(`current compile mode is ${this.compileMode}`);
}
analyzeBuildConfig() {
return __awaiter(this, void 0, void 0, function* () {
if (this.userConfig.devBuildConfig) {
let { name, path, env } = this.userConfig.devBuildConfig;
path = isAbsolute(path) ? path : join(process.cwd(), path);
if (!existsSync(path)) {
this.logger.warn(`Cant't find dev build config. Path is ${path}`);
return;
}
this.logger.debug(`Find dev build config. Path is ${path}`);
let bc = yield loadModule(path);
if (bc == undefined) {
return;
}
let finalBc = {};
if (typeof bc === 'function') {
finalBc = bc(env);
this.devBuildConfig = finalBc;
return;
}
this.devBuildConfig = bc;
if (this.userConfig.devBuildConfig.devProxy) {
this.devProxy = this.userConfig.devBuildConfig.devProxy;
}
}
if (!this.userConfig.buildConfig) {
switch (this.compileMode) {
case 'webpack':
this.buildConfigPath.push({ name: 'default', path: './webpack.config.js' });
break;
case 'vite':
this.buildConfigPath.push({ name: 'default', path: './vite.config.js' });
break;
default:
this.logger.warn(`Unknown compile mode ${this.compileMode}`);
break;
}
}
else {
this.userConfig.buildConfig.forEach((userBuildConfig) => {
// if (typeof userBuildConfig === 'string') {
// const name = this.getConfigName(userBuildConfig);
// this.buildConfigPath.push({name, path: userBuildConfig});
// }
if (typeof userBuildConfig === 'object') {
// const name = userBuildConfig.name;
// const path = userBuildConfig.path;
this.buildConfigPath.push(userBuildConfig);
}
});
}
this.buildConfigPath.forEach((config) => __awaiter(this, void 0, void 0, function* () {
let { name, path } = config;
path = isAbsolute(path) ? path : join(process.cwd(), path);
if (!existsSync(path)) {
this.logger.warn(`Cant't find build config. Path is ${path}`);
return;
}
this.logger.debug(`Find build config. Path is ${path}`);
let bc = yield loadModule(path);
if (bc == undefined) {
return;
}
let finalBc = {};
if (typeof bc === 'function') {
finalBc = bc(config.env);
this.buildConfig.push({ name: name, config: finalBc });
return;
}
this.buildConfig.push({ name: name, config: bc });
}));
});
}
getConfigName(name) {
name = name.replace('webpack.', '');
name = name.replace('.js', '');
name = name.replace('.ts', '');
return name;
}
}

14
packages/inula-cli/lib/enum/enum.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
export declare enum PluginType {
preset = "preset",
plugin = "plugin"
}
export declare enum ServiceStage {
uninitialized = 0,
constructor = 1,
init = 2,
initPlugins = 3,
initHooks = 4,
pluginReady = 5,
getConfig = 6,
run = 7
}

View File

@ -0,0 +1,16 @@
export var PluginType;
(function (PluginType) {
PluginType["preset"] = "preset";
PluginType["plugin"] = "plugin";
})(PluginType || (PluginType = {}));
export var ServiceStage;
(function (ServiceStage) {
ServiceStage[ServiceStage["uninitialized"] = 0] = "uninitialized";
ServiceStage[ServiceStage["constructor"] = 1] = "constructor";
ServiceStage[ServiceStage["init"] = 2] = "init";
ServiceStage[ServiceStage["initPlugins"] = 3] = "initPlugins";
ServiceStage[ServiceStage["initHooks"] = 4] = "initHooks";
ServiceStage[ServiceStage["pluginReady"] = 5] = "pluginReady";
ServiceStage[ServiceStage["getConfig"] = 6] = "getConfig";
ServiceStage[ServiceStage["run"] = 7] = "run";
})(ServiceStage || (ServiceStage = {}));

View File

@ -0,0 +1,43 @@
import PluginAPI, { IOpts } from './PluginAPI.js';
import { IHook, ICommand } from '../types/types.js';
import Hub from '../core/Hub';
import { Logger } from '../utils/logger.js';
interface pluginManagerOpts {
cwd: string;
hub: Hub;
logger: Logger;
}
export interface IPlugin {
id: string;
key: string;
path: string;
apply: Function;
}
export default class Plugin {
cwd: string;
builtInPlugins: string[];
userPlugins: string[];
commands: {
[name: string]: ICommand | string;
};
hooksByPluginPath: {
[id: string]: IHook[];
};
hooks: {
[key: string]: IHook[];
};
store: {
[key: string]: any;
};
hub: Hub;
logger: Logger;
registerFunction: Function[];
[key: string]: any;
constructor(opts: pluginManagerOpts);
getPluginPaths(builtInPlugins: string[], userPlugins: string[] | undefined): string[];
setStore(name: string, initialValue: any): void;
register(builtInPlugins: string[], userPlugins: string[] | undefined): Promise<void>;
createPluginAPI(opts: IOpts): PluginAPI;
initHook(): void;
}
export {};

View File

@ -0,0 +1,116 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import resolve from 'resolve';
import chalk from 'chalk';
import PluginAPI from './PluginAPI.js';
import { loadModule } from '../utils/loadModule.js';
export default class Plugin {
constructor(opts) {
this.builtInPlugins = [];
this.userPlugins = [];
this.commands = {};
this.hooksByPluginPath = {};
this.hooks = {};
this.store = {};
this.registerFunction = [];
this.cwd = opts.cwd || process.cwd();
this.hub = opts.hub;
this.logger = opts.logger;
}
getPluginPaths(builtInPlugins, userPlugins) {
const paths = [];
paths.push(...builtInPlugins);
if (userPlugins) {
paths.push(...userPlugins);
}
// 获取所有插件文件的绝对路径
const absPaths = paths.map(path => {
return resolve.sync(path, {
basedir: this.cwd,
extensions: ['.js', '.ts'],
});
});
return absPaths;
}
setStore(name, initialValue) {
const store = this.store;
if (this.store[name]) {
return;
}
store[name] = initialValue;
}
register(builtInPlugins, userPlugins) {
return __awaiter(this, void 0, void 0, function* () {
const paths = this.getPluginPaths(builtInPlugins, userPlugins);
this.hub.pluginPaths = paths;
const objs = paths.map(path => {
const api = this.createPluginAPI({
path,
manager: this,
logger: this.logger,
});
return {
path,
api,
};
});
for (const obj of objs) {
const module = yield loadModule(obj.path);
if (module) {
try {
module(obj.api);
}
catch (err) {
if (err instanceof Error) {
this.logger.error(chalk.red(err.message));
if (err.stack) {
this.logger.error(err.stack);
}
}
}
}
}
});
}
// todo 给API换个名字
createPluginAPI(opts) {
const pluginAPI = new PluginAPI(opts);
// 为PluginAPI添加代理
// 除了PluginAPI自有的方法之外为开发者提供更丰富的api
return new Proxy(pluginAPI, {
get: (target, prop) => {
if (['userConfig', 'devBuildConfig', 'buildConfig', 'compileMode', 'packageJson', 'cwd'].includes(prop)) {
return typeof this.hub[prop] === 'function'
? this.hub[prop].bind(this.hub)
: this.hub[prop];
}
if (['setStore', 'logger', 'commands'].includes(prop)) {
return typeof this[prop] === 'function'
? this[prop].bind(this)
: this[prop];
}
return target[prop];
},
});
}
initHook() {
Object.keys(this.hooksByPluginPath).forEach(path => {
const hooks = this.hooksByPluginPath[path];
hooks.forEach(hook => {
const { name } = hook;
hook.pluginId = path;
if (!this.hooks[name]) {
this.hooks[name] = [];
}
this.hooks[name].push(hook);
});
});
}
}

View File

@ -0,0 +1,20 @@
import Plugin from './Plugin.js';
import { IHook, ICommand } from '../types/types.js';
import { Logger } from '../utils/logger.js';
export interface IOpts {
path: string;
manager: Plugin;
logger: Logger;
}
export default class PluginAPI {
path: string;
manager: Plugin;
logger: Logger;
[key: string]: any;
constructor(opts: IOpts);
register(hook: IHook): void;
registerCommand(command: ICommand): void;
registerHook(hook: IHook): void;
registerMethod(fn: Function): void;
applyHook(name: string, args?: any): Promise<any>;
}

View File

@ -0,0 +1,50 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
export default class PluginAPI {
constructor(opts) {
this.path = opts.path;
this.manager = opts.manager;
this.logger = opts.logger;
}
register(hook) {
if (!this.manager.hooksByPluginPath[this.path]) {
this.manager.hooksByPluginPath[this.path] = [];
}
this.manager.hooksByPluginPath[this.path].push(hook);
}
registerCommand(command) {
const { name } = command;
this.manager.commands[name] = command;
if (command.initialState) {
this.manager.setStore(name, command.initialState);
}
}
registerHook(hook) {
this.register(hook);
}
registerMethod(fn) {
this.manager.registerFunction.push(fn);
}
applyHook(name, args) {
return __awaiter(this, void 0, void 0, function* () {
const hooks = this.manager.hooks[name] || [];
let config = undefined;
for (const hook of hooks) {
if (this.manager.store[name]) {
config = this.manager.store[name];
}
if (hook.fn) {
yield hook.fn(args, config);
}
}
return this.manager.store[name];
});
}
}

128
packages/inula-cli/lib/types/types.d.ts vendored Normal file
View File

@ -0,0 +1,128 @@
/// <reference types="node" />
import { PackageJSON } from 'resolve';
import yargsParser from 'yargs-parser';
import { Logger } from '../utils/logger.js';
import type * as http from 'http';
import type * as express from 'express';
interface Request extends express.Request {
}
interface Response extends express.Response {
}
export interface IDep {
[name: string]: string;
}
export interface IPackage {
name?: string;
dependencies?: IDep;
devDependencies?: IDep;
[key: string]: any;
}
export interface IPlugin {
id: string;
key: string;
path: string;
apply: Function;
config?: IPluginConfig;
isPreset?: boolean;
}
export interface IPluginConfig {
default?: any;
onChange?: string | Function;
}
export interface IHook {
name: string;
fn?: {
(state: any, config: any): void;
};
pluginId?: string;
}
export interface ICommand {
name: string;
description?: string;
details?: string;
initialState?: any;
fn: {
(args: yargsParser.Arguments, config: any): void;
};
}
export interface IConfig {
plugins?: string[];
[key: string]: any;
}
interface applyHookConfig<T = any> {
name: string;
config?: T;
}
export interface API {
cwd: string;
logger: Logger;
userConfig: IConfig;
buildConfig: any;
devBuildConfig: any;
compileMode: string;
commands: string[];
packageJson: PackageJSON;
registerCommand: {
(command: ICommand): void;
};
registerHook: {
(hook: IHook): void;
};
registerMethod: {
(method: Function): void;
};
applyHook: {
(opts: applyHookConfig): void;
};
setStore: {
(name: string, initialState: any): void;
};
}
export interface RemoteProxy {
target: string;
localPort?: number;
localStatic?: StaticFileMatcher[];
fowardingURL?: string[];
}
export interface StaticFileMatcher {
url: string;
local: string;
}
export interface UserConfig {
mock?: MockConfig;
proxy?: RemoteProxy;
plugins?: string[];
compileMode?: string;
buildConfig?: BuildConfig[];
devBuildConfig?: DevBuildConfig;
}
export interface MockConfig {
enableMock?: boolean;
mockPath?: string;
}
export interface DevBuildConfig {
name: string;
path: string;
args?: object;
env?: object;
devProxy?: DevProxy;
}
export interface DevProxy {
target: string;
matcher: ((pathname: string, req: Request) => boolean);
onProxyRes: (proxyRes: http.IncomingMessage, req: Request, res: Response) => void;
}
export interface BuildConfig {
name: string;
path: string;
args?: object;
env?: object;
}
export declare type ExportUserConfig = UserConfig | Promise<UserConfig>;
export declare function defineConfig(config: ExportUserConfig): ExportUserConfig;
export interface Arguments {
_: Array<string | number>;
'--'?: Array<string | number>;
[argName: string]: any;
}
export {};

View File

@ -0,0 +1,3 @@
export function defineConfig(config) {
return config;
}

View File

@ -0,0 +1,2 @@
declare const buildConfig: (fileName: string, format?: 'esm' | 'cjs') => Promise<string>;
export default buildConfig;

View File

@ -0,0 +1,61 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import path from 'path';
import fs from 'fs';
import { build as esbuild } from 'esbuild';
const buildConfig = (fileName, format = 'esm') => __awaiter(void 0, void 0, void 0, function* () {
// 外部依赖不构建参与构建,减少执行时间
const pluginExternalDeps = {
name: 'plugin-external-deps',
setup(build) {
build.onResolve({ filter: /.*/ }, args => {
const id = args.path;
if (id[0] !== '.' && !path.isAbsolute(id)) {
return {
external: true,
};
}
return {};
});
},
};
// 将文件中的路径改成确定路径,避免执行时调用错误
const pluginReplaceImport = {
name: 'plugin-replace-import-meta',
setup(build) {
build.onLoad({ filter: /\.[jt]s$/ }, args => {
const contents = fs.readFileSync(args.path, 'utf8');
// 替换import路径
contents.replace(/\bimport\.meta\.url\b/g, JSON.stringify(`file://${args.path}`));
// 替换当前目录路径
contents.replace(/\b__dirname\b/g, JSON.stringify(path.dirname(args.path)));
// 替换当前文件路径
contents.replace(/\b__filename\b/g, JSON.stringify(args.path));
return {
loader: args.path.endsWith('.ts') ? 'ts' : 'js',
contents: contents
};
});
},
};
const result = yield esbuild({
entryPoints: [fileName],
outfile: 'out.js',
write: false,
platform: 'node',
bundle: true,
format,
metafile: true,
plugins: [pluginExternalDeps, pluginReplaceImport],
});
const { text } = result.outputFiles[0];
return text;
});
export default buildConfig;

View File

@ -0,0 +1 @@
export default function dynamicImport(filePath: string): Promise<any>;

View File

@ -0,0 +1,16 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
export default function dynamicImport(filePath) {
return __awaiter(this, void 0, void 0, function* () {
let importPath = filePath;
importPath = 'file:///' + importPath;
return yield import(importPath);
});
}

View File

@ -0,0 +1 @@
export default function initializeEnv(): void;

View File

@ -0,0 +1,19 @@
import { join } from 'path';
import { readFileSync, existsSync } from 'fs';
import { parse } from 'dotenv';
export default function initializeEnv() {
const envPath = join(process.cwd(), '.env');
const localEnvPath = join(process.cwd(), '.local.env');
loadEnv(envPath);
if (process.env.NODE_ENV === 'development') {
loadEnv(localEnvPath);
}
}
function loadEnv(envPath) {
if (existsSync(envPath)) {
const parsed = parse(readFileSync(envPath, 'utf-8')) || {};
Object.keys(parsed).forEach(key => {
process.env[key] = parsed[key];
});
}
}

View File

@ -0,0 +1 @@
export declare function loadModule<T>(filePath: string): Promise<T | undefined>;

View File

@ -0,0 +1,59 @@
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { join, isAbsolute } from 'path';
import fs from 'fs';
import buildConfig from './build.js';
import { createRequire } from 'module';
import dynamicImport from './dynamicImport.js';
const require = createRequire(import.meta.url);
export function loadModule(filePath) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
filePath = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath);
const isTsFile = filePath.endsWith('ts');
const isJsFile = filePath.endsWith('js');
let content;
// js文件可以直接通过import引用
if (isJsFile) {
content = (_a = (yield dynamicImport(filePath))) === null || _a === void 0 ? void 0 : _a.default;
}
// 如果是ts文件需要先转为js文件再读取
if (isTsFile) {
const code = yield buildConfig(filePath, 'esm');
content = yield getTypescriptModule(code, filePath);
}
return content;
});
}
function getTypescriptModule(code, filePath, isEsm = true) {
var _a, _b;
return __awaiter(this, void 0, void 0, function* () {
const tempFile = `${filePath}.${isEsm ? 'm' : 'c'}js`;
let content = null;
// todo 臨時文件管理
fs.writeFileSync(tempFile, code);
delete require.cache[require.resolve(tempFile)];
try {
const raw = isEsm ? yield dynamicImport(tempFile) : require(tempFile);
content = (_a = raw === null || raw === void 0 ? void 0 : raw.default) !== null && _a !== void 0 ? _a : raw;
}
catch (err) {
fs.unlinkSync(tempFile);
if (err instanceof Error) {
err.message = err.message.replace(tempFile, filePath);
err.stack = (_b = err.stack) === null || _b === void 0 ? void 0 : _b.replace(tempFile, filePath);
}
throw err;
}
// todo 刪除失敗加日誌
fs.unlinkSync(tempFile);
return content;
});
}

View File

@ -0,0 +1,2 @@
import { PackageJSON } from 'resolve';
export declare const loadPkg: (path: string) => PackageJSON;

View File

@ -0,0 +1,6 @@
import fs from 'fs';
export const loadPkg = (path) => {
const packageJson = fs.readFileSync(path, 'utf8');
const packageData = JSON.parse(packageJson);
return packageData;
};

View File

@ -0,0 +1,14 @@
export declare enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3
}
export declare class Logger {
private readonly level;
constructor(level?: LogLevel);
debug(message: string): void;
info(message: string): void;
warn(message: string): void;
error(message: string, error?: Error): void;
}

View File

@ -0,0 +1,37 @@
export var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
LogLevel[LogLevel["INFO"] = 1] = "INFO";
LogLevel[LogLevel["WARN"] = 2] = "WARN";
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
})(LogLevel || (LogLevel = {}));
export class Logger {
constructor(level) {
if (level !== undefined) {
this.level = level;
}
else {
this.level = LogLevel.INFO;
}
}
debug(message) {
if (this.level <= LogLevel.DEBUG) {
console.debug(`[DEBUG] ${message}`);
}
}
info(message) {
if (this.level <= LogLevel.INFO) {
console.info(`[INFO] ${message}`);
}
}
warn(message) {
if (this.level <= LogLevel.WARN) {
console.warn(`[WARN] ${message}`);
}
}
error(message, error) {
if (this.level <= LogLevel.ERROR) {
console.error(`[ERROR] ${message}`, error || '');
}
}
}

View File

@ -0,0 +1,6 @@
declare const _default: (app: {
_router: {
stack: any[];
};
}) => void;
export default _default;

View File

@ -0,0 +1,102 @@
import chokidar from 'chokidar';
import bodyParser from 'body-parser';
import { globSync } from 'glob';
import { join } from 'path';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const mockDir = join(process.cwd(), 'mock');
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head'];
const jsonParser = bodyParser.json();
const urlencodedParser = bodyParser.urlencoded({
extended: true,
});
// 读取 mock 文件夹下的 js 文件
function getMocksFile() {
const mockFiles = globSync('**/*.js', {
cwd: mockDir,
});
let ret = mockFiles.reduce((mocks, mockFile) => {
if (!mockFile.startsWith('_')) {
mocks = Object.assign(Object.assign({}, mocks), require(join(mockDir, mockFile)));
console.log('mockFile', require(join(mockDir, mockFile)));
}
return mocks;
}, {});
return ret;
}
function generateRoutes(app) {
let mockStartIndex = app._router.stack.length, mocks = {};
try {
mocks = getMocksFile();
}
catch (error) {
console.error('Generate mock routes error', error);
}
for (const mockItem in mocks) {
if (Object.prototype.hasOwnProperty.call(mocks, mockItem)) {
try {
const trimMockItemArr = mockItem
.replace(/(^\s*)|(\s*$)/g, '')
.replace(/\s+/g, ' ')
.split(' ');
const respond = mocks[mockItem];
let mockType = 'get', mockUrl;
if (trimMockItemArr.length === 1) {
mockUrl = trimMockItemArr[0];
}
else {
[mockType, mockUrl] = trimMockItemArr;
}
const mockTypeLowerCase = mockType.toLowerCase();
if (!HTTP_METHODS.includes(mockTypeLowerCase)) {
throw new Error(`Invalid HTTP request method ${mockType} for path ${mockUrl}`);
}
app[mockTypeLowerCase](mockUrl, [jsonParser, urlencodedParser], respond instanceof Function
? respond
: (_req, res) => {
res.send(respond);
});
}
catch (error) {
console.error(error);
}
}
}
return {
mockRoutesLength: app._router.stack.length - mockStartIndex,
mockStartIndex: mockStartIndex,
};
}
// 清除 mock 文件下的 require 缓存
function cleanRequireCache() {
Object.keys(require.cache).forEach(key => {
if (key.includes(mockDir)) {
delete require.cache[require.resolve(key)];
}
});
}
export default (app) => {
const mockRoutes = generateRoutes(app);
let { mockRoutesLength } = mockRoutes;
let { mockStartIndex } = mockRoutes;
// 监听 mock 文件夹下文件变化
chokidar
.watch(mockDir, {
ignoreInitial: true,
})
.on('all', (event, _path) => {
if (event === 'change' || event === 'add') {
try {
// 删除中间件映射
app._router.stack.splice(mockStartIndex, mockRoutesLength);
cleanRequireCache();
const mockRoutes = generateRoutes(app);
mockRoutesLength = mockRoutes.mockRoutesLength;
mockStartIndex = mockRoutes.mockStartIndex;
}
catch (error) {
console.error(error);
}
}
});
};

View File

@ -0,0 +1 @@
export default function readDirectory(directoryPath: string): string[];

View File

@ -0,0 +1,26 @@
import fs from 'fs';
import path from 'path';
export default function readDirectory(directoryPath) {
const filesArray = [];
const traverseDirectory = (directoryPath) => {
const files = fs.readdirSync(directoryPath);
for (const file of files) {
const filePath = path.join(directoryPath, file);
if (fs.statSync(filePath).isDirectory()) {
// 如果是目录,则递归读取该目录下的所有文件
traverseDirectory(filePath);
}
else {
if (filePath.startsWith('.')) {
continue;
}
// 如果是文件,则将其全路径添加到数组中
if (filePath.endsWith('.js')) {
filesArray.push(filePath);
}
}
}
};
traverseDirectory(directoryPath);
return filesArray;
}

View File

@ -0,0 +1,3 @@
import { API } from '../types/types';
declare const _default: (app: any, api: API) => void;
export default _default;

View File

@ -0,0 +1,11 @@
import { createProxyMiddleware } from 'http-proxy-middleware';
export default (app, api) => {
const { devProxy } = api.userConfig.devBuildConfig;
app.use(createProxyMiddleware(devProxy.matcher, {
target: devProxy.target,
secure: false,
changeOrigin: true,
ws: false,
onProxyRes: devProxy.onProxyRes
}));
};

View File

@ -0,0 +1,4 @@
export declare function parseRequireDeps(filePath: string): string[];
export declare const isWindows: boolean;
export declare function cleanRequireCache(cacheKey: string): void;
export declare function copyFile(targetPath: string, sourcePath: string): void;

View File

@ -0,0 +1,52 @@
import { dirname } from 'path';
import { readFileSync, writeFileSync } from 'fs';
import resolve from 'resolve';
// @ts-ignore
import crequire from 'crequire';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
function parse(filePath) {
const content = readFileSync(filePath, 'utf-8');
return crequire(content)
.map(o => o.path)
.filter(path => path.charAt(0) === '.')
.map(path => resolve.sync(path, {
basedir: dirname(filePath),
extensions: ['.tsx', '.ts', '.jsx', '.js'],
}));
}
export function parseRequireDeps(filePath) {
const paths = [filePath];
const ret = [filePath];
while (paths.length) {
const extraPaths = parse(paths.shift()).filter(path => !ret.includes(path));
if (extraPaths.length) {
paths.push(...extraPaths);
ret.push(...extraPaths);
}
}
return ret;
}
export const isWindows = typeof process !== 'undefined' && process.platform === 'win32';
export function cleanRequireCache(cacheKey) {
const cachePath = isWindows ? cacheKey.replace(/\//g, '\\') : cacheKey;
if (require.cache[cachePath]) {
const cacheParent = require.cache[cachePath].parent;
let i = (cacheParent === null || cacheParent === void 0 ? void 0 : cacheParent.children.length) || 0;
while (i--) {
if (cacheParent.children[i].id === cachePath) {
cacheParent.children.splice(i, 1);
}
}
delete require.cache[cachePath];
}
}
export function copyFile(targetPath, sourcePath) {
try {
const fileContent = readFileSync(sourcePath);
writeFileSync(targetPath, fileContent);
}
catch (error) {
console.error('Copy file failed.', error);
}
}

View File

@ -0,0 +1,67 @@
{
"name": "inula-cli",
"version": "0.0.1",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc"
},
"bin": {
"inula-cli": "bin/start.js"
},
"files":[
"bin",
"lib",
"template",
"package.json",
"tsconfig.json"
],
"author": "",
"license": "ISC",
"type": "module",
"devDependencies": {
"@types/body-parser": "^1.19.2",
"@types/chalk": "^2.2.0",
"@types/express": "^4.17.17",
"@types/inquirer": "^9.0.3",
"@types/lodash": "^4.14.194",
"@types/node": "^18.16.1",
"@types/resolve": "^1.20.2",
"@types/webpack": "^5.28.1",
"@types/yargs-parser": "^21.0.0"
},
"dependencies": {
"@babel/runtime": "^7.22.5",
"@types/http-proxy-middleware": "^1.0.0",
"@types/jest": "^29.5.2",
"babel-eslint": "^10.1.0",
"chalk": "^4.0.0",
"chokidar": "^3.5.3",
"crequire": "^1.8.1",
"deepmerge": "^4.3.1",
"dotenv": "^16.0.3",
"esbuild": "^0.18.17",
"express": "^4.18.2",
"glob": "^10.3.3",
"http-proxy-middleware": "^2.0.6",
"inquirer": "^9.2.7",
"install": "^0.13.0",
"jest": "^29.5.0",
"lodash": "^4.17.21",
"resolve": "^1.22.3",
"ts-jest": "^29.1.0",
"ts-node": "^10.9.1",
"vite": "4.4.2",
"webpack": "^5.0.0",
"webpack-dev-server": "^4.13.3",
"yargs-parser": "^21.1.1"
},
"types": "lib/types/types.d.ts",
"exports": {
".": {
"types": "./lib/types/types.d.ts",
"import": "./lib/types/types.js",
"require": "./lib/types/types.js"
}
}
}

View File

@ -0,0 +1,37 @@
import webpack from 'webpack';
import { build } from 'vite';
export default (api: any) => {
api.registerCommand({
name: 'build',
description: 'build application for production',
initialState: api.buildConfig,
fn: async function (args: any, state: any) {
switch (api.compileMode) {
case 'webpack':
if (state) {
api.applyHook({ name: 'beforeCompile', args: state });
state.forEach((s: any) => {
webpack(s.config, (err: any, stats: any) => {
// api.applyHook({ name: 'afterCompile' });
if (err || stats.hasErrors()) {
api.logger.error(`Build failed.err: ${err}, stats:${stats}`);
}
});
});
} else {
api.logger.error(`Build failed. Can't find build config.`);
}
break;
case 'vite':
if (state) {
api.applyHook({ name: 'beforeCompile' });
build(state);
} else {
api.logger.error(`Build failed. Can't find build config.`);
}
break;
}
},
});
};

View File

@ -0,0 +1,65 @@
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import { createServer } from 'vite';
import { API } from '../../../types/types';
import setupProxy from '../../../utils/setupProxy.js';
export default (api: API) => {
api.registerCommand({
name: 'dev',
description: 'build application for development',
initialState: api.devBuildConfig,
fn: async function (args: any, state: any) {
api.applyHook({ name: 'beforeDevConfig' });
switch (api.compileMode) {
case 'webpack':
if (state) {
api.applyHook({ name: 'beforeDevCompile', config: state });
const compiler = webpack(state);
const devServerOptions: WebpackDevServer.Configuration = {
client: {
overlay: false,
},
host: 'localhost',
port: '8888',
open: true,
historyApiFallback: true,
};
if (api.userConfig.devBuildConfig.devProxy) {
devServerOptions.onBeforeSetupMiddleware = (devServer: WebpackDevServer) => {
setupProxy(devServer.app, api)
}
}
api.applyHook({
name: 'beforeStartDevServer',
config: { compiler: compiler, devServerOptions: devServerOptions },
});
const server = new WebpackDevServer(compiler, devServerOptions);
server.startCallback((err: any) => {
api.applyHook({ name: 'afterStartDevServer' });
});
} else {
api.logger.error("Can't find config");
}
break;
case 'vite':
if (state) {
await createServer(state)
.then(server => {
return server.listen();
})
.then(server => {
server.printUrls();
});
} else {
api.logger.error("Can't find config");
}
break;
}
},
});
};

View File

@ -0,0 +1,104 @@
import { API } from '../../../types/types';
import yargsParser from 'yargs-parser';
import inquirer from 'inquirer';
import fs from 'fs';
import path from 'path';
import { copyFile } from '../../../utils/util.js';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default (api: API) => {
api.registerCommand({
name: 'generate',
description: 'generate template',
fn: async (args: yargsParser.Arguments) => {
if (args._[0] === 'g') {
args._.shift();
}
if (args._.length === 0) {
api.logger.warn("Can't find any generate options.");
return;
}
switch (args._[0]) {
case 'jest':
args._.shift();
const isESM = api.packageJson['type'] === 'module';
await generateJest(args, api.cwd, isESM);
}
},
});
};
const generateJest = async (args: yargsParser.Arguments, cwd: string, isESM: boolean) => {
let isTs: boolean = false;
if (args['ts']) {
isTs = true;
} else {
const answers = await inquirer.prompt([
{
name: 'useTs',
message: 'Do you want to use TypeScript',
type: 'confirm',
},
]);
isTs = answers['useTs'];
}
if (checkJestConfigExist(cwd)) {
console.log('The jest config is exist.');
return;
}
const testRootPath = path.join(cwd, 'test');
if (!fs.existsSync(testRootPath)) {
fs.mkdirSync(testRootPath);
}
let templateDir = path.resolve(__dirname, '../../../../template/test');
// 如果是TS, 拷贝ts
if (isTs) {
templateDir = path.join(templateDir, 'ts');
copyTestTemplate(cwd, testRootPath, templateDir);
}
// 拷贝mjs
if (!isTs && isESM) {
templateDir = path.join(templateDir, 'mjs');
copyTestTemplate(cwd, testRootPath, templateDir);
}
// 拷贝cjs
if (!isTs && !isESM) {
templateDir = path.join(templateDir, 'cjs');
copyTestTemplate(cwd, testRootPath, templateDir);
}
};
function checkJestConfigExist(cwd: string): boolean {
const items = fs.readdirSync(cwd);
for (const item of items) {
const itemPath = path.resolve(cwd, item);
const states = fs.statSync(itemPath);
if (states.isFile() && item.startsWith('jest.config')) {
return true;
}
}
return false;
}
function copyTestTemplate(cwd: string, testRootPath: string, templateDir: string) {
const items = fs.readdirSync(templateDir);
for (const item of items) {
const itemPath = path.resolve(templateDir, item);
if (item.startsWith('jest.config')) {
copyFile(path.join(cwd, item), itemPath);
} else {
copyFile(path.join(testRootPath, item), itemPath);
}
}
}

View File

@ -0,0 +1,36 @@
import chalk from 'chalk';
import lodash from 'lodash';
function getDescriptions(commands: any) {
return Object.keys(commands)
.filter(name => typeof commands[name] !== 'string')
.map(name => {
return getDescription(commands[name]);
});
}
function getDescription(command: any) {
return ` ${chalk.green(lodash.padEnd(command.name, 10))}${command.description || ''}`;
}
function padLeft(str: string) {
return str
.split('\n')
.map((line: string) => ` ${line}`)
.join('\n');
}
export default (api: any) => {
api.registerCommand({
name: 'help',
description: 'show command helps',
fn: (args: any, config: any) => {
console.log(`
Usage: inula-cli <command> [options]
${getDescriptions(api.commands).join('\n')}
`);
},
});
};

View File

@ -0,0 +1,16 @@
import { createRequire } from 'module';
import mockServer from '../../../utils/mockServer.js';
const require = createRequire(import.meta.url);
export default (api: any) => {
api.registerHook({
name: 'beforeStartDevServer',
fn: async (state: any) => {
const { compiler, devServerOptions } = state;
devServerOptions.setupMiddlewares = (middlewares: any, devServer: { app: any }) => {
mockServer(devServer.app);
return middlewares;
};
},
});
};

View File

@ -0,0 +1,44 @@
import express from 'express';
import { createRequire } from 'module';
import { createProxyMiddleware } from 'http-proxy-middleware';
const require = createRequire(import.meta.url);
export default (api: any) => {
api.registerCommand({
name: 'proxy',
description: 'remote proxy',
initialState: api.userConfig.remoteProxy,
fn: async function (args: any, state: any) {
if (!state) {
api.logger.error(`Invalid proxy config!`);
return;
}
const app = express();
const proxyConfig = state;
const staticList = proxyConfig.localStatic;
staticList.forEach(function (value: { url: any; local: string }) {
app.use(value.url, express.static(value.local));
});
const remoteProxy = createProxyMiddleware(proxyConfig.fowardingURL, {
target: proxyConfig.target,
secure: false,
autoRewrite: true,
protocolRewrite: 'http',
ws: true,
hostRewrite: '',
preserveHeaderKeyCase: true,
proxyTimeout: 5 * 60 * 60 * 1000,
timeout: 5 * 60 * 60 * 1000,
onError: handleProxyError,
});
function handleProxyError(err: any) {
api.logger.error('Local proxy error. Error is ', err);
}
app.use(remoteProxy);
app.listen(proxyConfig.localPort, () => {
api.logger.info(`Start proxy client on http://localhost:${proxyConfig.localPort}`);
});
},
});
};

View File

@ -0,0 +1,13 @@
import { API } from '../../../types/types';
import jest from 'jest';
import yargsParser from 'yargs-parser';
export default (api: API) => {
api.registerCommand({
name: 'jest',
description: 'run jest test',
fn: async (args: yargsParser.Arguments, config: any) => {
await jest.run();
},
});
};

View File

@ -0,0 +1,31 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { API } from '../../../types/types';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
interface PackageJson {
version: string;
}
const pkgPath = path.resolve(__dirname, '../../../../package.json');
// 读取 package.json 文件
const packageJson = fs.readFileSync(pkgPath, 'utf8');
// 解析 JSON 格式的数据
const packageData: PackageJson = JSON.parse(packageJson);
// 获取版本号
const version = packageData.version;
export default (api: API) => {
api.registerCommand({
name: 'version',
description: 'show inula-cli version',
fn: () => {
api.logger.info(`Inula-cli version is ${version}.`);
},
});
};

View File

@ -0,0 +1,73 @@
import chalk from 'chalk';
import yargsParser from 'yargs-parser';
import Hub from '../core/Hub.js';
import initializeEnv from '../utils/initializeEnv.js';
import { Logger, LogLevel } from '../utils/logger.js';
export default async function run() {
const args: yargsParser.Arguments = yargsParser(process.argv.slice(2));
const alias: Record<string, string> = {
h: 'help',
v: 'version',
g: 'generate',
};
let command: string | number | undefined = args._[0];
if (!command) {
if (args['v'] || args['version']) {
command = 'v';
}
if (args['h'] || args['help']) {
command = 'h';
}
}
const aliasCommand: string | undefined = alias[command];
if (aliasCommand) {
command = aliasCommand;
}
initializeEnv();
if (command === 'version' || command === 'help') {
process.env.INNER_COMMAND = "true"
}
switch (command) {
case 'build':
process.env.NODE_ENV = 'production';
break;
case 'dev':
process.env.NODE_ENV = 'development';
break;
default:
process.env.NODE_ENV = 'development';
break;
}
let enableDebug: boolean = false;
if (process.env.DEBUG === "true") {
enableDebug = true;
}
const logger: Logger = new Logger(enableDebug ? LogLevel.DEBUG : LogLevel.INFO);
try {
new Hub({
logger: logger,
}).run({
command,
args,
});
} catch (err: unknown) {
if (err instanceof Error) {
logger.error(chalk.red(err.message));
if (err.stack) {
logger.error(err.stack);
}
process.exit(1);
}
}
}

View File

@ -0,0 +1,108 @@
import { existsSync } from 'fs';
import { extname, join } from 'path';
import { parseRequireDeps, cleanRequireCache } from '../utils/util.js';
import deepmerge from 'deepmerge';
import { loadModule } from '../utils/loadModule.js';
import { Logger } from '../utils/logger.js';
import { UserConfig } from '../types/types.js';
interface ConfigOpts {
cwd: string;
isLocal?: boolean;
logger: Logger;
}
const DEFAULT_CONFIG_FILES = ['.inula.ts', '.inula.js'];
export default class Config {
cwd: string;
isLocal: boolean;
configFile?: string | null;
logger: Logger;
constructor(opts: ConfigOpts) {
this.cwd = opts.cwd || process.cwd();
this.isLocal = opts.isLocal || process.env.NODE_ENV === 'development';
this.logger = opts.logger;
}
async getUserConfig(): Promise<UserConfig> {
const configFile: string | null = this.getConfigFile();
if (configFile === null) {
this.logger.debug(`Can't find .inula.ts or .inula.js in ${this.cwd}`);
return {};
}
this.configFile = configFile;
if (configFile) {
let envConfigFile: string | undefined = undefined;
if (process.env.RUNNING_MODE) {
envConfigFile = this.addModePath(configFile, process.env.RUNNING_MODE);
}
// 配置文件的来源
// 1、默认的configFile 如.inula.ts
// 2、带环境变量的configFile 如.inula.cloud.ts
// 3、dev模式 包含local 如.inula.local.ts
const files: string[] = [configFile];
if (envConfigFile && existsSync(envConfigFile)) {
files.push(envConfigFile);
}
if (this.isLocal) {
const localConfigFile = this.addModePath(configFile, 'local');
if (existsSync(localConfigFile)) {
files.push(localConfigFile);
}
}
this.logger.debug(`Find user config files ${files}`);
// 依次加载配置文件中的依赖并刷新require中的缓存
const requireDeps = files.reduce((deps: string[], file) => {
deps = deps.concat(parseRequireDeps(file));
return deps;
}, []);
requireDeps.forEach(cleanRequireCache);
const configs = await this.requireConfigs(files);
return this.mergeConfig(...configs);
} else {
return {};
}
}
getConfigFile(): string | null {
const configFileList: string[] = DEFAULT_CONFIG_FILES.map(f => join(this.cwd, f));
for (let configFile of configFileList) {
if (existsSync(configFile)) {
return configFile;
}
}
return null;
}
addModePath(file: string, mode: string) {
const ext = extname(file);
return file.replace(new RegExp(`${ext}$`), `.${mode}${ext}`);
}
async requireConfigs(configFiles: string[]) {
const configs: UserConfig[] = [];
for (const file in configFiles) {
const content: UserConfig | undefined = await loadModule<UserConfig>(configFiles[file]);
if (content) {
configs.push(content);
}
}
return configs;
}
mergeConfig(...configs: UserConfig[]) {
let ret: UserConfig = {};
for (const config of configs) {
ret = deepmerge<UserConfig>(ret, config);
}
return ret;
}
}

View File

@ -0,0 +1,220 @@
import { join, isAbsolute } from 'path';
import Config from '../config/Config.js';
import { BuildConfig, DevBuildConfig, DevProxy, ICommand, UserConfig } from '../types/types.js';
import { ServiceStage } from '../enum/enum.js';
import Plugin from '../plugin/Plugin.js';
import { appendFile, existsSync } from 'fs';
import { createRequire } from 'module';
import { Logger, LogLevel } from '../utils/logger.js';
import { loadModule } from '../utils/loadModule.js';
import readDirectory from '../utils/readDirectory.js';
import path from 'path';
import { fileURLToPath } from 'url';
import yargsParser from 'yargs-parser';
import { PackageJSON } from 'resolve';
import { loadPkg } from '../utils/loadPkg.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
interface HubOpts {
cwd?: string;
logger?: Logger;
}
export default class Hub {
args: any;
cwd: string;
env: string | undefined;
configManager: Config;
userConfig: UserConfig = {};
packageJson: PackageJSON;
stage: ServiceStage = ServiceStage.uninitialized;
buildConfig: {name:string, config: object}[] = [];
pluginManager: Plugin;
buildConfigPath: BuildConfig[] = [];
devBuildConfig: object = {};
compileMode: string = '';
builtInPlugins: string[] = [];
pluginPaths: string[] = [];
devProxy: DevProxy | null = null;
logger: Logger;
[key: string]: any;
constructor(opts: HubOpts) {
this.setStage(ServiceStage.constructor);
this.cwd = opts.cwd || process.cwd();
this.env = process.env.NODE_ENV;
if (!opts.logger) {
this.logger = new Logger(LogLevel.INFO);
} else {
this.logger = opts.logger;
}
this.packageJson = loadPkg(path.join(this.cwd, './package.json'));
this.configManager = new Config({
cwd: this.cwd,
isLocal: this.env === 'development',
logger: this.logger,
});
this.pluginManager = new Plugin({
cwd: this.cwd,
hub: this,
logger: this.logger,
});
}
setStage(stage: ServiceStage) {
this.stage = stage;
}
async init() {
this.setStage(ServiceStage.init);
// 获取用户配置
this.userConfig = await this.configManager.getUserConfig();
// 设置编译模式
this.setCompileMode()
// 获取编译配置
await this.analyzeBuildConfig();
this.setStage(ServiceStage.initPlugins);
this.builtInPlugins = this.getBuiltInPlugins();
await this.pluginManager.register(this.builtInPlugins, this.userConfig.plugins);
this.setStage(ServiceStage.initHooks);
this.pluginManager.initHook();
}
getBuiltInPlugins(): string[] {
return readDirectory(path.resolve(__dirname, '../builtInPlugins'));
}
async run({ command, args }: { command: string | number; args: yargsParser.Arguments }) {
args._ = args._ || [];
if (args._[0] === command) {
args._.shift();
}
this.args = args;
await this.init();
this.setStage(ServiceStage.run);
return this.runCommand({ command, args });
}
async runCommand({ command, args }: { command: string | number; args: yargsParser.Arguments }) {
this.logger.debug(`run command ${command}`);
const commands =
typeof this.pluginManager.commands[command] === 'string'
? this.pluginManager.commands[this.pluginManager.commands[command] as string]
: this.pluginManager.commands[command];
if (commands === undefined) {
this.logger.error(`Invalid command ${command}`)
return
}
const { fn } = commands as ICommand;
return fn(args, this.pluginManager.store[command]);
}
setCompileMode() {
this.compileMode = this.userConfig.compileMode || 'webpack';
this.logger.debug(`current compile mode is ${this.compileMode}`);
}
async analyzeBuildConfig() {
if (this.userConfig.devBuildConfig) {
let { name, path, env } = this.userConfig.devBuildConfig;
path = isAbsolute(path) ? path : join(process.cwd(), path);
if (!existsSync(path)) {
this.logger.warn(`Cant't find dev build config. Path is ${path}`);
return;
}
this.logger.debug(`Find dev build config. Path is ${path}`);
let bc = await loadModule<object | Function>(path);
if (bc == undefined) {
return;
}
let finalBc = {};
if (typeof bc === 'function') {
finalBc = bc(env)
this.devBuildConfig = finalBc;
return;
}
this.devBuildConfig = bc;
if (this.userConfig.devBuildConfig.devProxy) {
this.devProxy = this.userConfig.devBuildConfig.devProxy;
}
}
if (!this.userConfig.buildConfig) {
switch (this.compileMode) {
case 'webpack':
this.buildConfigPath.push({name:'default', path:'./webpack.config.js'})
break;
case 'vite':
this.buildConfigPath.push({name:'default', path:'./vite.config.js'})
break;
default:
this.logger.warn(`Unknown compile mode ${this.compileMode}`);
break;
}
} else {
this.userConfig.buildConfig.forEach((userBuildConfig) => {
// if (typeof userBuildConfig === 'string') {
// const name = this.getConfigName(userBuildConfig);
// this.buildConfigPath.push({name, path: userBuildConfig});
// }
if (typeof userBuildConfig === 'object') {
// const name = userBuildConfig.name;
// const path = userBuildConfig.path;
this.buildConfigPath.push(userBuildConfig);
}
})
}
this.buildConfigPath.forEach(async (config) => {
let {name, path} = config;
path = isAbsolute(path) ? path : join(process.cwd(), path);
if (!existsSync(path)) {
this.logger.debug(`Cant't find build config. Path is ${path}`);
return;
}
this.logger.debug(`Find build config. Path is ${path}`);
let bc = await loadModule<object | Function >(path);
if (bc == undefined) {
return;
}
let finalBc = {};
if (typeof bc === 'function') {
finalBc = bc(config.env)
this.buildConfig.push({name: name, config: finalBc});
return;
}
this.buildConfig.push({name: name, config: bc});
})
}
getConfigName(name: string): string {
name = name.replace('webpack.', '');
name = name.replace('.js', '');
name = name.replace('.ts', '');
return name
}
}

View File

@ -0,0 +1,15 @@
export enum PluginType {
preset = 'preset',
plugin = 'plugin',
}
export enum ServiceStage {
uninitialized,
constructor,
init,
initPlugins,
initHooks,
pluginReady,
getConfig,
run,
}

View File

@ -0,0 +1,152 @@
import resolve from 'resolve';
import chalk from 'chalk';
import PluginAPI, { IOpts } from './PluginAPI.js';
import { IHook, ICommand } from '../types/types.js';
import Hub from '../core/Hub';
import { loadModule } from '../utils/loadModule.js';
import { Logger } from '../utils/logger.js';
interface pluginManagerOpts {
cwd: string;
hub: Hub;
logger: Logger;
}
interface PluginObj {
path: string;
api: PluginAPI;
}
export interface IPlugin {
id: string;
key: string;
path: string;
apply: Function;
}
export default class Plugin {
cwd: string;
builtInPlugins: string[] = [];
userPlugins: string[] = [];
commands: {
[name: string]: ICommand | string;
} = {};
hooksByPluginPath: {
[id: string]: IHook[];
} = {};
hooks: {
[key: string]: IHook[];
} = {};
store: {
[key: string]: any;
} = {};
hub: Hub;
logger: Logger;
registerFunction: Function[] = [];
// 解决调用this[props]时ts提示属性未知
[key: string]: any;
constructor(opts: pluginManagerOpts) {
this.cwd = opts.cwd || process.cwd();
this.hub = opts.hub;
this.logger = opts.logger;
}
getPluginPaths(builtInPlugins: string[], userPlugins: string[] | undefined): string[] {
const paths: string[] = [];
paths.push(...builtInPlugins);
if (userPlugins) {
paths.push(...userPlugins);
}
// 获取所有插件文件的绝对路径
const absPaths: string[] = paths.map(path => {
return resolve.sync(path, {
basedir: this.cwd,
extensions: ['.js', '.ts'],
});
});
return absPaths;
}
setStore(name: string, initialValue: any) {
const store = this.store;
if (this.store[name]) {
return;
}
store[name] = initialValue;
}
async register(builtInPlugins: string[], userPlugins: string[] | undefined) {
const paths = this.getPluginPaths(builtInPlugins, userPlugins);
this.hub.pluginPaths = paths;
const objs: PluginObj[] = paths.map(path => {
const api: PluginAPI = this.createPluginAPI({
path,
manager: this,
logger: this.logger,
});
return {
path,
api,
};
});
for (const obj of objs) {
const module: Function | undefined = await loadModule(obj.path);
if (module) {
try {
module(obj.api);
} catch (err: unknown) {
if (err instanceof Error) {
this.logger.error(chalk.red(err.message));
if (err.stack) {
this.logger.error(err.stack);
}
}
}
}
}
}
// todo 给API换个名字
createPluginAPI(opts: IOpts): PluginAPI {
const pluginAPI = new PluginAPI(opts);
// 为PluginAPI添加代理
// 除了PluginAPI自有的方法之外为开发者提供更丰富的api
return new Proxy(pluginAPI, {
get: (target: PluginAPI, prop: string) => {
if (['userConfig', 'devBuildConfig', 'buildConfig', 'compileMode', 'packageJson', 'cwd'].includes(prop)) {
return typeof this.hub[prop] === 'function'
? this.hub[prop].bind(this.hub)
: this.hub[prop];
}
if (['setStore', 'logger', 'commands'].includes(prop)) {
return typeof this[prop] === 'function'
? this[prop].bind(this)
: this[prop];
}
return target[prop];
},
});
}
initHook() {
Object.keys(this.hooksByPluginPath).forEach(path => {
const hooks = this.hooksByPluginPath[path];
hooks.forEach(hook => {
const { name } = hook;
hook.pluginId = path;
if (!this.hooks[name]) {
this.hooks[name] = [];
}
this.hooks[name].push(hook);
});
});
}
}

View File

@ -0,0 +1,60 @@
import Plugin from './Plugin.js';
import { IHook, ICommand } from '../types/types.js';
import { Logger } from '../utils/logger.js';
export interface IOpts {
path: string;
manager: Plugin;
logger: Logger;
}
export default class PluginAPI {
path: string;
manager: Plugin;
logger: Logger;
[key: string]: any;
constructor(opts: IOpts) {
this.path = opts.path;
this.manager = opts.manager;
this.logger = opts.logger;
}
register(hook: IHook) {
if (!this.manager.hooksByPluginPath[this.path]) {
this.manager.hooksByPluginPath[this.path] = [];
}
this.manager.hooksByPluginPath[this.path].push(hook);
}
registerCommand(command: ICommand) {
const { name } = command;
this.manager.commands[name] = command;
if (command.initialState) {
this.manager.setStore(name, command.initialState);
}
}
registerHook(hook: IHook) {
this.register(hook);
}
registerMethod(fn: Function) {
this.manager.registerFunction.push(fn);
}
async applyHook(name: string, args?: any ) {
const hooks: IHook[] = this.manager.hooks[name] || [];
let config: any = undefined;
for (const hook of hooks) {
if (this.manager.store[name]) {
config = this.manager.store[name];
}
if (hook.fn) {
await hook.fn(args, config);
}
}
return this.manager.store[name];
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
testEnvironment: 'node', // 使用 Node.js 环境进行测试
// 匹配的测试文件模式
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],
};

View File

@ -0,0 +1,5 @@
function sum(a, b) {
return a + b;
}
module.exports = sum;

View File

@ -0,0 +1,7 @@
const sum = require('./sum');
describe('sum', () => {
it('should add two numbers', () => {
expect(sum(1, 2)).toEqual(3);
});
});

View File

@ -0,0 +1,6 @@
export default {
testEnvironment: 'node', // 使用 Node.js 环境进行测试
// 匹配的测试文件模式
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],
};

View File

@ -0,0 +1,5 @@
function sum(a, b) {
return a + b;
}
export default sum;

View File

@ -0,0 +1,7 @@
import sum from './sum';
describe('sum', () => {
it('should add two numbers', () => {
expect(sum(1, 2)).toEqual(3);
});
});

View File

@ -0,0 +1,7 @@
export default {
preset: 'ts-jest',
testEnvironment: 'node', // 使用 Node.js 环境进行测试
// 匹配的测试文件模式
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).(js?(x)|ts?(x))$'],
};

View File

@ -0,0 +1,7 @@
import sum from './sum';
describe('sum', () => {
it('should add two numbers', () => {
expect(sum(1, 2)).toEqual(3);
});
});

View File

@ -0,0 +1,5 @@
function sum(a: number, b: number): number {
return a + b;
}
export default sum;

View File

@ -0,0 +1,153 @@
import { PackageJSON } from 'resolve';
import yargsParser from 'yargs-parser';
import { Logger } from '../utils/logger.js';
import type * as http from 'http';
import type * as express from 'express';
interface Request extends express.Request {
}
interface Response extends express.Response {
}
export interface IDep {
[name: string]: string;
}
export interface IPackage {
name?: string;
dependencies?: IDep;
devDependencies?: IDep;
[key: string]: any;
}
export interface IPlugin {
id: string;
key: string;
path: string;
apply: Function;
config?: IPluginConfig;
isPreset?: boolean;
}
export interface IPluginConfig {
default?: any;
onChange?: string | Function;
}
export interface IHook {
// 触发事件名称
name: string;
fn?: {
(state: any, config: any): void;
};
pluginId?: string;
}
export interface ICommand {
name: string;
description?: string;
details?: string;
initialState?: any;
fn: {
(args: yargsParser.Arguments, config: any): void;
};
}
export interface IConfig {
plugins?: string[];
[key: string]: any;
}
interface applyHookConfig<T = any> {
name: string;
config?: T;
}
export interface API {
cwd: string;
logger: Logger;
userConfig: IConfig;
buildConfig: any;
devBuildConfig: any;
compileMode: string;
commands: string[];
packageJson: PackageJSON;
registerCommand: {
(command: ICommand): void;
};
registerHook: {
(hook: IHook): void;
};
registerMethod: {
(method: Function): void;
}
applyHook: {
(opts: applyHookConfig): void;
};
setStore: {
(name: string, initialState: any): void;
};
}
export interface RemoteProxy {
target: string;
localPort?: number;
localStatic?: StaticFileMatcher[];
fowardingURL?: string[];
}
export interface StaticFileMatcher {
url: string;
local: string;
}
export interface UserConfig {
mock?: MockConfig;
proxy?: RemoteProxy;
plugins?: string[];
compileMode?: string;
buildConfig?: BuildConfig[];
devBuildConfig?: DevBuildConfig;
}
export interface MockConfig {
enableMock?: boolean;
mockPath?: string;
}
export interface DevBuildConfig {
name: string;
path: string;
args?: object;
env?: object;
devProxy?: DevProxy;
}
export interface DevProxy {
target: string;
matcher: ((pathname: string, req: Request) => boolean);
onProxyRes: (proxyRes: http.IncomingMessage, req: Request, res: Response) => void;
}
export interface BuildConfig {
name: string;
path: string;
args?: object;
env?: object;
}
export type ExportUserConfig = UserConfig | Promise<UserConfig>;
export function defineConfig(config: ExportUserConfig): ExportUserConfig {
return config;
}
export interface Arguments {
_: Array<string | number>;
'--'?: Array<string | number>;
[argName: string]: any;
}

View File

@ -0,0 +1,62 @@
import path from 'path';
import fs from 'fs';
import { build as esbuild, Plugin } from 'esbuild';
const buildConfig = async (fileName: string, format: 'esm' | 'cjs' = 'esm'): Promise<string> => {
// 外部依赖不构建参与构建,减少执行时间
const pluginExternalDeps: Plugin = {
name: 'plugin-external-deps',
setup(build) {
build.onResolve({ filter: /.*/ }, args => {
const id = args.path;
if (id[0] !== '.' && !path.isAbsolute(id)) {
return {
external: true,
};
}
return {};
});
},
};
// 将文件中的路径改成确定路径,避免执行时调用错误
const pluginReplaceImport: Plugin = {
name: 'plugin-replace-import-meta',
setup(build) {
build.onLoad({ filter: /\.[jt]s$/ }, args => {
const contents: string = fs.readFileSync(args.path, 'utf8');
// 替换import路径
contents.replace(/\bimport\.meta\.url\b/g, JSON.stringify(`file://${args.path}`));
// 替换当前目录路径
contents.replace(/\b__dirname\b/g, JSON.stringify(path.dirname(args.path)));
// 替换当前文件路径
contents.replace(/\b__filename\b/g, JSON.stringify(args.path));
return {
loader: args.path.endsWith('.ts') ? 'ts' : 'js',
contents: contents
};
});
},
};
const result = await esbuild({
entryPoints: [fileName],
outfile: 'out.js',
write: false,
platform: 'node',
bundle: true,
format,
metafile: true,
plugins: [pluginExternalDeps, pluginReplaceImport],
});
const { text } = result.outputFiles[0];
return text;
};
export default buildConfig;

View File

@ -0,0 +1,6 @@
export default async function dynamicImport(filePath: string) {
let importPath = filePath;
importPath = 'file:///' + importPath;
return await import(importPath);
}

View File

@ -0,0 +1,21 @@
import { join } from 'path';
import { readFileSync, existsSync } from 'fs';
import { parse } from 'dotenv';
export default function initializeEnv(): void {
const envPath: string = join(process.cwd(), '.env');
const localEnvPath: string = join(process.cwd(), '.local.env');
loadEnv(envPath);
if (process.env.NODE_ENV === 'development') {
loadEnv(localEnvPath);
}
}
function loadEnv(envPath: string): void {
if (existsSync(envPath)) {
const parsed = parse(readFileSync(envPath, 'utf-8')) || {};
Object.keys(parsed).forEach(key => {
process.env[key] = parsed[key];
});
}
}

View File

@ -0,0 +1,57 @@
import { pathToFileURL } from 'url';
import { join, isAbsolute } from 'path';
import fs from 'fs';
import buildConfig from './build.js';
import { createRequire } from 'module';
import dynamicImport from './dynamicImport.js';
const require = createRequire(import.meta.url);
export async function loadModule<T>(filePath: string): Promise<T | undefined> {
filePath = isAbsolute(filePath) ? filePath : join(process.cwd(), filePath);
const isTsFile: boolean = filePath.endsWith('ts');
const isJsFile: boolean = filePath.endsWith('js');
let content: T | undefined;
// js文件可以直接通过import引用
if (isJsFile) {
content = (await dynamicImport(filePath))?.default;
}
// 如果是ts文件需要先转为js文件再读取
if (isTsFile) {
const code = await buildConfig(filePath, 'esm');
content = await getTypescriptModule(code, filePath);
}
return content;
}
async function getTypescriptModule(code: string, filePath: string, isEsm = true) {
const tempFile = `${filePath}.${isEsm ? 'm' : 'c'}js`;
let content = null;
// todo 臨時文件管理
fs.writeFileSync(tempFile, code);
delete require.cache[require.resolve(tempFile)];
try {
const raw = isEsm ? await dynamicImport(tempFile) : require(tempFile);
content = raw?.default ?? raw;
} catch (err: unknown) {
fs.unlinkSync(tempFile);
if (err instanceof Error) {
err.message = err.message.replace(tempFile, filePath);
err.stack = err.stack?.replace(tempFile, filePath);
}
throw err;
}
// todo 刪除失敗加日誌
fs.unlinkSync(tempFile);
return content;
}

View File

@ -0,0 +1,8 @@
import fs from 'fs';
import { PackageJSON } from 'resolve';
export const loadPkg = (path: string): PackageJSON => {
const packageJson = fs.readFileSync(path, 'utf8');
const packageData: PackageJSON = JSON.parse(packageJson);
return packageData;
};

View File

@ -0,0 +1,42 @@
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
}
export class Logger {
private readonly level: LogLevel;
constructor(level?: LogLevel) {
if (level !== undefined) {
this.level = level;
} else {
this.level = LogLevel.INFO;
}
}
debug(message: string): void {
if (this.level <= LogLevel.DEBUG) {
console.debug(`[DEBUG] ${message}`);
}
}
info(message: string): void {
if (this.level <= LogLevel.INFO) {
console.info(`[INFO] ${message}`);
}
}
warn(message: string): void {
if (this.level <= LogLevel.WARN) {
console.warn(`[WARN] ${message}`);
}
}
error(message: string, error?: Error): void {
if (this.level <= LogLevel.ERROR) {
console.error(`[ERROR] ${message}`, error || '');
}
}
}

View File

@ -0,0 +1,130 @@
import chokidar from 'chokidar';
import bodyParser from 'body-parser';
import {globSync} from 'glob';
import { join } from 'path';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const mockDir = join(process.cwd(), 'mock');
const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head'];
const jsonParser = bodyParser.json();
const urlencodedParser = bodyParser.urlencoded({
extended: true,
});
interface Mock {
[key: string]: any;
}
// 读取 mock 文件夹下的 js 文件
function getMocksFile() {
const mockFiles = globSync('**/*.js', {
cwd: mockDir,
});
let ret = mockFiles.reduce((mocks: any, mockFile: string) => {
if (!mockFile.startsWith('_')) {
mocks = {
...mocks,
...require(join(mockDir, mockFile)),
};
console.log('mockFile', require(join(mockDir, mockFile)));
}
return mocks;
}, {});
return ret;
}
function generateRoutes(app: any) {
let mockStartIndex = app._router.stack.length,
mocks: Mock = {};
try {
mocks = getMocksFile();
} catch (error) {
console.error('Generate mock routes error', error);
}
for (const mockItem in mocks) {
if (Object.prototype.hasOwnProperty.call(mocks, mockItem)) {
try {
const trimMockItemArr = mockItem
.replace(/(^\s*)|(\s*$)/g, '')
.replace(/\s+/g, ' ')
.split(' ');
const respond = mocks[mockItem];
let mockType = 'get',
mockUrl;
if (trimMockItemArr.length === 1) {
mockUrl = trimMockItemArr[0];
} else {
[mockType, mockUrl] = trimMockItemArr;
}
const mockTypeLowerCase = mockType.toLowerCase();
if (!HTTP_METHODS.includes(mockTypeLowerCase)) {
throw new Error(`Invalid HTTP request method ${mockType} for path ${mockUrl}`);
}
app[mockTypeLowerCase](
mockUrl,
[jsonParser, urlencodedParser],
respond instanceof Function
? respond
: (_req: any, res: { send: (arg0: any) => void }) => {
res.send(respond);
}
);
} catch (error) {
console.error(error);
}
}
}
return {
mockRoutesLength: app._router.stack.length - mockStartIndex,
mockStartIndex: mockStartIndex,
};
}
// 清除 mock 文件下的 require 缓存
function cleanRequireCache() {
Object.keys(require.cache).forEach(key => {
if (key.includes(mockDir)) {
delete require.cache[require.resolve(key)];
}
});
}
export default (app: { _router: { stack: any[] } }) => {
const mockRoutes = generateRoutes(app);
let { mockRoutesLength } = mockRoutes;
let { mockStartIndex } = mockRoutes;
// 监听 mock 文件夹下文件变化
chokidar
.watch(mockDir, {
ignoreInitial: true,
})
.on('all', (event: string, _path: any) => {
if (event === 'change' || event === 'add') {
try {
// 删除中间件映射
app._router.stack.splice(mockStartIndex, mockRoutesLength);
cleanRequireCache();
const mockRoutes = generateRoutes(app);
mockRoutesLength = mockRoutes.mockRoutesLength;
mockStartIndex = mockRoutes.mockStartIndex;
} catch (error) {
console.error(error);
}
}
});
};

View File

@ -0,0 +1,28 @@
import fs from 'fs';
import path from 'path';
export default function readDirectory(directoryPath: string): string[] {
const filesArray: string[] = [];
const traverseDirectory = (directoryPath: string) => {
const files = fs.readdirSync(directoryPath);
for (const file of files) {
const filePath = path.join(directoryPath, file);
if (fs.statSync(filePath).isDirectory()) {
// 如果是目录,则递归读取该目录下的所有文件
traverseDirectory(filePath);
} else {
if (filePath.startsWith('.')) {
continue;
}
// 如果是文件,则将其全路径添加到数组中
if (filePath.endsWith('.js')) {
filesArray.push(filePath);
}
}
}
};
traverseDirectory(directoryPath);
return filesArray;
}

View File

@ -0,0 +1,13 @@
import { createProxyMiddleware } from 'http-proxy-middleware';
import { API } from '../types/types';
export default (app: any, api: API) => {
const { devProxy } = api.userConfig.devBuildConfig;
app.use(createProxyMiddleware(devProxy.matcher, {
target: devProxy.target,
secure: false,
changeOrigin: true,
ws: false,
onProxyRes: devProxy.onProxyRes
}));
}

View File

@ -0,0 +1,60 @@
import { dirname } from 'path';
import { readFileSync, writeFileSync } from 'fs';
import resolve from 'resolve';
// @ts-ignore
import crequire from 'crequire'
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
function parse(filePath: string): string[] {
const content = readFileSync(filePath, 'utf-8');
return (crequire(content) as any[])
.map<string>(o => o.path)
.filter(path => path.charAt(0) === '.')
.map(path =>
resolve.sync(path, {
basedir: dirname(filePath),
extensions: ['.tsx', '.ts', '.jsx', '.js'],
})
);
}
export function parseRequireDeps(filePath: string): string[] {
const paths: string[] = [filePath];
const ret: string[] = [filePath];
while (paths.length) {
const extraPaths = parse(paths.shift()!).filter(path => !ret.includes(path));
if (extraPaths.length) {
paths.push(...extraPaths);
ret.push(...extraPaths);
}
}
return ret;
}
export const isWindows = typeof process !== 'undefined' && process.platform === 'win32';
export function cleanRequireCache(cacheKey: string): void {
const cachePath = isWindows ? cacheKey.replace(/\//g, '\\') : cacheKey;
if (require.cache[cachePath]) {
const cacheParent = (require.cache[cachePath] as any).parent;
let i = cacheParent?.children.length || 0;
while (i--) {
if (cacheParent!.children[i].id === cachePath) {
cacheParent!.children.splice(i, 1);
}
}
delete require.cache[cachePath];
}
}
export function copyFile(targetPath: string, sourcePath: string): void {
try {
const fileContent = readFileSync(sourcePath);
writeFileSync(targetPath, fileContent);
} catch (error) {
console.error('Copy file failed.', error);
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
testEnvironment: 'node', // 使用 Node.js 环境进行测试
// 匹配的测试文件模式
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],
};

View File

@ -0,0 +1,5 @@
function sum(a, b) {
return a + b;
}
module.exports = sum;

View File

@ -0,0 +1,7 @@
const sum = require('./sum');
describe('sum', () => {
it('should add two numbers', () => {
expect(sum(1, 2)).toEqual(3);
});
});

View File

@ -0,0 +1,6 @@
export default {
testEnvironment: 'node', // 使用 Node.js 环境进行测试
// 匹配的测试文件模式
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],
};

View File

@ -0,0 +1,5 @@
function sum(a, b) {
return a + b;
}
export default sum;

View File

@ -0,0 +1,7 @@
import sum from './sum';
describe('sum', () => {
it('should add two numbers', () => {
expect(sum(1, 2)).toEqual(3);
});
});

View File

@ -0,0 +1,7 @@
export default {
preset: 'ts-jest',
testEnvironment: 'node', // 使用 Node.js 环境进行测试
// 匹配的测试文件模式
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).(js?(x)|ts?(x))$'],
};

View File

@ -0,0 +1,7 @@
import sum from './sum';
describe('sum', () => {
it('should add two numbers', () => {
expect(sum(1, 2)).toEqual(3);
});
});

View File

@ -0,0 +1,5 @@
function sum(a: number, b: number): number {
return a + b;
}
export default sum;

View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es2015",
"module": "esnext",
"sourceMap": false,
"outDir": "./lib",
"strict": true,
"allowJs": true,
"skipLibCheck": true,
"declaration": true,
"moduleResolution": "node",
"esModuleInterop": true,
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/*.spec.ts", "./src/template/**/*"],
"ts-node": {
"esm": true,
},
}