Match-id-2ee2e4235032ec1ddfc09c904eae3c6341d20ee2
This commit is contained in:
parent
9be2395a60
commit
331f8a91c4
|
@ -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声明规范
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
webpack/
|
||||
public/
|
|
@ -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)
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import run from '../lib/cli/cli.js';
|
||||
|
||||
run();
|
|
@ -0,0 +1,2 @@
|
|||
declare const _default: (api: any) => void;
|
||||
export default _default;
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
import { API } from '../../../types/types';
|
||||
declare const _default: (api: API) => void;
|
||||
export default _default;
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
import { API } from '../../../types/types';
|
||||
declare const _default: (api: API) => void;
|
||||
export default _default;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
declare const _default: (api: any) => void;
|
||||
export default _default;
|
|
@ -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')}
|
||||
`);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
declare const _default: (api: any) => void;
|
||||
export default _default;
|
|
@ -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;
|
||||
};
|
||||
}),
|
||||
});
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
declare const _default: (api: any) => void;
|
||||
export default _default;
|
|
@ -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}`);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
import { API } from '../../../types/types';
|
||||
declare const _default: (api: API) => void;
|
||||
export default _default;
|
|
@ -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();
|
||||
}),
|
||||
});
|
||||
};
|
|
@ -0,0 +1,3 @@
|
|||
import { API } from '../../../types/types';
|
||||
declare const _default: (api: API) => void;
|
||||
export default _default;
|
|
@ -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}.`);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export default function run(): Promise<void>;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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 {};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 = {}));
|
|
@ -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 {};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
}
|
|
@ -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];
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 {};
|
|
@ -0,0 +1,3 @@
|
|||
export function defineConfig(config) {
|
||||
return config;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
declare const buildConfig: (fileName: string, format?: 'esm' | 'cjs') => Promise<string>;
|
||||
export default buildConfig;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
export default function dynamicImport(filePath: string): Promise<any>;
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export default function initializeEnv(): void;
|
|
@ -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];
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export declare function loadModule<T>(filePath: string): Promise<T | undefined>;
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
import { PackageJSON } from 'resolve';
|
||||
export declare const loadPkg: (path: string) => PackageJSON;
|
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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 || '');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
declare const _default: (app: {
|
||||
_router: {
|
||||
stack: any[];
|
||||
};
|
||||
}) => void;
|
||||
export default _default;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export default function readDirectory(directoryPath: string): string[];
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import { API } from '../types/types';
|
||||
declare const _default: (app: any, api: API) => void;
|
||||
export default _default;
|
|
@ -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
|
||||
}));
|
||||
};
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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')}
|
||||
`);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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}`);
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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}.`);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
export enum PluginType {
|
||||
preset = 'preset',
|
||||
plugin = 'plugin',
|
||||
}
|
||||
|
||||
export enum ServiceStage {
|
||||
uninitialized,
|
||||
constructor,
|
||||
init,
|
||||
initPlugins,
|
||||
initHooks,
|
||||
pluginReady,
|
||||
getConfig,
|
||||
run,
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
testEnvironment: 'node', // 使用 Node.js 环境进行测试
|
||||
|
||||
// 匹配的测试文件模式
|
||||
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
function sum(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
module.exports = sum;
|
|
@ -0,0 +1,7 @@
|
|||
const sum = require('./sum');
|
||||
|
||||
describe('sum', () => {
|
||||
it('should add two numbers', () => {
|
||||
expect(sum(1, 2)).toEqual(3);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
testEnvironment: 'node', // 使用 Node.js 环境进行测试
|
||||
|
||||
// 匹配的测试文件模式
|
||||
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
function sum(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export default sum;
|
|
@ -0,0 +1,7 @@
|
|||
import sum from './sum';
|
||||
|
||||
describe('sum', () => {
|
||||
it('should add two numbers', () => {
|
||||
expect(sum(1, 2)).toEqual(3);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node', // 使用 Node.js 环境进行测试
|
||||
|
||||
// 匹配的测试文件模式
|
||||
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).(js?(x)|ts?(x))$'],
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import sum from './sum';
|
||||
|
||||
describe('sum', () => {
|
||||
it('should add two numbers', () => {
|
||||
expect(sum(1, 2)).toEqual(3);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
function sum(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export default sum;
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
|
@ -0,0 +1,6 @@
|
|||
export default async function dynamicImport(filePath: string) {
|
||||
let importPath = filePath;
|
||||
|
||||
importPath = 'file:///' + importPath;
|
||||
return await import(importPath);
|
||||
}
|
|
@ -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];
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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 || '');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}));
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
testEnvironment: 'node', // 使用 Node.js 环境进行测试
|
||||
|
||||
// 匹配的测试文件模式
|
||||
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
function sum(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
module.exports = sum;
|
|
@ -0,0 +1,7 @@
|
|||
const sum = require('./sum');
|
||||
|
||||
describe('sum', () => {
|
||||
it('should add two numbers', () => {
|
||||
expect(sum(1, 2)).toEqual(3);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
testEnvironment: 'node', // 使用 Node.js 环境进行测试
|
||||
|
||||
// 匹配的测试文件模式
|
||||
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).js?(x)'],
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
function sum(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export default sum;
|
|
@ -0,0 +1,7 @@
|
|||
import sum from './sum';
|
||||
|
||||
describe('sum', () => {
|
||||
it('should add two numbers', () => {
|
||||
expect(sum(1, 2)).toEqual(3);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node', // 使用 Node.js 环境进行测试
|
||||
|
||||
// 匹配的测试文件模式
|
||||
testMatch: ['**/__tests__/**/*.js?(x)', '**/?(*.)+(spec|test).(js?(x)|ts?(x))$'],
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
import sum from './sum';
|
||||
|
||||
describe('sum', () => {
|
||||
it('should add two numbers', () => {
|
||||
expect(sum(1, 2)).toEqual(3);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
function sum(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export default sum;
|
|
@ -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,
|
||||
},
|
||||
}
|
Loading…
Reference in New Issue