Merge pull request '新增inula max 内容' (#8) from xiaohuoni/inula:master into master
This commit is contained in:
commit
68ad2d3e9c
|
@ -8,3 +8,4 @@ pnpm-lock.yaml
|
||||||
build
|
build
|
||||||
/packages/inula-router/connectRouter
|
/packages/inula-router/connectRouter
|
||||||
/packages/inula-router/router
|
/packages/inula-router/router
|
||||||
|
.inula-max
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineConfig } from 'father';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
cjs: {
|
||||||
|
output: 'dist',
|
||||||
|
ignores: ['src/client/**'],
|
||||||
|
},
|
||||||
|
esm: {
|
||||||
|
input: 'src/client',
|
||||||
|
output: 'client/client',
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
/node_modules
|
||||||
|
/packages/**/node_modules
|
||||||
|
/packages/**/dist
|
||||||
|
/packages/**/tsconfig.tsbuildinfo
|
||||||
|
/packages/**/src/**/fixtures/*/dist
|
||||||
|
/packages/**/src/**/fixtures/*/.umi
|
||||||
|
/packages/**/src/**/fixtures/*/.umi-production
|
||||||
|
.umi
|
||||||
|
.inula
|
||||||
|
.umi-production
|
||||||
|
.inula-production
|
||||||
|
.umi-test
|
||||||
|
.inula-test
|
||||||
|
dist
|
||||||
|
es
|
||||||
|
lib
|
||||||
|
.turbo
|
||||||
|
.idea
|
||||||
|
playwright-report
|
||||||
|
/packages/inula/client
|
||||||
|
.env.local
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Inula
|
||||||
|
|
||||||
|
## 项目简介
|
||||||
|
|
||||||
|
inula-max 是一个关注业务需求,以开发体验为主的前端框架,集成 openInula 全生态。
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
你可以通过以下步骤快速开始使用 Inula:
|
||||||
|
|
||||||
|
```base
|
||||||
|
npx inula-max init [dir]
|
||||||
|
```
|
||||||
|
|
||||||
|
初始化一个 Inula 项目,目录可选,一般操作是新建一个空白文件夹,再执行 `npx inula-max init` 即可。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
### openInula 官方组件
|
||||||
|
|
||||||
|
#### 状态管理器
|
||||||
|
|
||||||
|
Inula-X 是 openInula 默认提供的状态管理器,无需额外引入三方库,就可以简单实现跨组件/页面共享状态。
|
||||||
|
|
||||||
|
|
||||||
|
#### 请求
|
||||||
|
|
||||||
|
Inula-request 涵盖常见的网络请求方式,并提供动态轮询钩子函数给用户更便捷的定制化请求体验。
|
||||||
|
|
||||||
|
#### 国际化
|
||||||
|
|
||||||
|
Inula-intl 提供了国际化功能,涵盖了基本的国际化组件和钩子函数,便于用户在构建国际化能力时方便操作。
|
||||||
|
|
||||||
|
### 其他能力
|
||||||
|
|
||||||
|
#### antd
|
||||||
|
|
||||||
|
Ant Design 是一个功能丰富的 UI 组件库。
|
||||||
|
|
||||||
|
#### ProComponents
|
||||||
|
|
||||||
|
ProComponents 是一个让中后台开发更简单的工具。
|
||||||
|
|
||||||
|
#### AIGC
|
||||||
|
|
||||||
|
AIGC 是一个使用 Azure Api 对接 OpenAI ChatGPT 4 模型的能力,可以快速使用 AIGC 助力业务开发。
|
||||||
|
|
||||||
|
## 更多信息
|
||||||
|
|
||||||
|
请访问 [OpenInula 文档](https://docs.openinula.net/) 获取更多详细信息。
|
||||||
|
|
||||||
|
## 贡献
|
||||||
|
|
||||||
|
欢迎贡献代码和提出问题!请查看 [贡献指南](CONTRIBUTING.md) 了解如何参与项目。
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本项目基于 [MIT](LICENSE) 许可证开源。
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// setNodeTitle
|
||||||
|
process.title = 'inula';
|
||||||
|
// Use magic to suppress node deprecation warnings
|
||||||
|
// See: https://github.com/nodejs/node/blob/master/lib/internal/process/warning.js#L77
|
||||||
|
// @ts-ignore
|
||||||
|
process.noDeprecation = '1';
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
require('../dist/cli')
|
||||||
|
.run()
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
export declare enum ApplyPluginsType {
|
||||||
|
compose = "compose",
|
||||||
|
modify = "modify",
|
||||||
|
event = "event"
|
||||||
|
}
|
||||||
|
interface IPlugin {
|
||||||
|
path?: string;
|
||||||
|
apply: Record<string, any>;
|
||||||
|
}
|
||||||
|
export declare class PluginManager {
|
||||||
|
opts: {
|
||||||
|
validKeys: string[];
|
||||||
|
};
|
||||||
|
hooks: {
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
constructor(opts: {
|
||||||
|
validKeys: string[];
|
||||||
|
});
|
||||||
|
register(plugin: IPlugin): void;
|
||||||
|
getHooks(keyWithDot: string): any;
|
||||||
|
applyPlugins({ key, type, initialValue, args, async, }: {
|
||||||
|
key: string;
|
||||||
|
type: ApplyPluginsType;
|
||||||
|
initialValue?: any;
|
||||||
|
args?: object;
|
||||||
|
async?: boolean;
|
||||||
|
}): any;
|
||||||
|
static create(opts: {
|
||||||
|
validKeys: string[];
|
||||||
|
plugins: IPlugin[];
|
||||||
|
}): PluginManager;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
|
//# sourceMappingURL=plugin.d.ts.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/client/plugin.ts"],"names":[],"mappings":"AAEA,oBAAY,gBAAgB;IAC1B,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,KAAK,UAAU;CAChB;AAED,UAAU,OAAO;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B;AAED,qBAAa,aAAa;IACxB,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAC9B,KAAK,EAAE;QACL,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB,CAAM;gBACK,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE;IAIzC,QAAQ,CAAC,MAAM,EAAE,OAAO;IAaxB,QAAQ,CAAC,UAAU,EAAE,MAAM;IAqB3B,YAAY,CAAC,EACX,GAAG,EACH,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,GACN,EAAE;QACD,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,gBAAgB,CAAC;QACvB,YAAY,CAAC,EAAE,GAAG,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;IAuFD,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,EAAE,CAAA;KAAE;CAShE"}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,7 @@
|
||||||
|
export declare function assert(value: unknown, message: string): void;
|
||||||
|
export declare function compose({ fns, args, }: {
|
||||||
|
fns: (Function | any)[];
|
||||||
|
args?: object;
|
||||||
|
}): any;
|
||||||
|
export declare function isPromiseLike(obj: any): boolean;
|
||||||
|
//# sourceMappingURL=utils.d.ts.map
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/client/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,QAErD;AAED,wBAAgB,OAAO,CAAC,EACtB,GAAG,EACH,IAAI,GACL,EAAE;IACD,GAAG,EAAE,CAAC,QAAQ,GAAG,GAAG,CAAC,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,OAMA;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,WAErC"}
|
|
@ -0,0 +1,20 @@
|
||||||
|
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
|
||||||
|
export function assert(value, message) {
|
||||||
|
if (!value) throw new Error(message);
|
||||||
|
}
|
||||||
|
export function compose(_ref) {
|
||||||
|
var fns = _ref.fns,
|
||||||
|
args = _ref.args;
|
||||||
|
if (fns.length === 1) {
|
||||||
|
return fns[0];
|
||||||
|
}
|
||||||
|
var last = fns.pop();
|
||||||
|
return fns.reduce(function (a, b) {
|
||||||
|
return function () {
|
||||||
|
return b(a, args);
|
||||||
|
};
|
||||||
|
}, last);
|
||||||
|
}
|
||||||
|
export function isPromiseLike(obj) {
|
||||||
|
return !!obj && _typeof(obj) === 'object' && typeof obj.then === 'function';
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { defineConfig } from 'inula-max';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
title: 'boilerplate',
|
||||||
|
});
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"name": "@example/boilerplate",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "inula-max build",
|
||||||
|
"build-analyze": "ANALYZE=1 inula-max build",
|
||||||
|
"dev": "inula-max dev",
|
||||||
|
"preview": "inula-max preview",
|
||||||
|
"setup": "inula-max setup",
|
||||||
|
"start": "npm run dev"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"inula-max": "link:.."
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useStore } from 'inula';
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
const store = useStore('hello');
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
hello {store.title}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
store.changeName();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
改名字啦
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { createStore } from 'inula';
|
||||||
|
|
||||||
|
export default createStore({
|
||||||
|
id: 'hello12',
|
||||||
|
actions: {
|
||||||
|
changeName: (state) => {
|
||||||
|
state.title = 'openinula';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
title: 'inulajs',
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { createStore } from 'inula';
|
||||||
|
|
||||||
|
export default createStore({
|
||||||
|
id: 'hello',
|
||||||
|
actions: {
|
||||||
|
changeName: (state) => {
|
||||||
|
state.title = 'openinula';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
title: 'inulajs',
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "./src/.inula-max/tsconfig.json",
|
||||||
|
"compilerOptions":{
|
||||||
|
"paths": {
|
||||||
|
"inula-max": [
|
||||||
|
"../../"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
import "inula/typings";
|
|
@ -0,0 +1,7 @@
|
||||||
|
try {
|
||||||
|
require.resolve('@umijs/lint/package.json');
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error('@umijs/lint is not built-in, please install it manually before run umi lint.');
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = process.env.LEGACY_ESLINT ? require('@umijs/lint/dist/config/eslint/legacy') : require('@umijs/lint/dist/config/eslint');
|
|
@ -0,0 +1,10 @@
|
||||||
|
// @ts-ignore
|
||||||
|
export * from '@@/exports';
|
||||||
|
export type {
|
||||||
|
IApi,
|
||||||
|
webpack,
|
||||||
|
IRoute,
|
||||||
|
UmiApiRequest,
|
||||||
|
UmiApiResponse,
|
||||||
|
} from '@aluni/types';
|
||||||
|
export * from './dist';
|
|
@ -0,0 +1,55 @@
|
||||||
|
{
|
||||||
|
"name": "inula-max",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "A Inulajs framework based on umi.",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "index.d.ts",
|
||||||
|
"bin": {
|
||||||
|
"inula-max": "bin/inula.js"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"assets",
|
||||||
|
"bin",
|
||||||
|
"client",
|
||||||
|
"dist",
|
||||||
|
"index.d.ts",
|
||||||
|
"plugin-utils.d.ts",
|
||||||
|
"plugin-utils.js",
|
||||||
|
"eslint.js",
|
||||||
|
"prettier.js"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "father build",
|
||||||
|
"dev": "father dev"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aluni/preset-inula": "0.0.5",
|
||||||
|
"@aluni/types": "^0.0.5",
|
||||||
|
"@umijs/bundler-utils": "4.0.88",
|
||||||
|
"@umijs/bundler-vite": "4.0.88",
|
||||||
|
"@umijs/bundler-webpack": "4.0.88",
|
||||||
|
"@umijs/core": "4.0.88",
|
||||||
|
"@umijs/lint": "4.0.88",
|
||||||
|
"@umijs/openapi": "^1.13.0",
|
||||||
|
"@umijs/preset-blocks": "0.0.4",
|
||||||
|
"@umijs/preset-umi": "4.0.88",
|
||||||
|
"@umijs/server": "4.0.88",
|
||||||
|
"@umijs/utils": "4.0.88",
|
||||||
|
"prettier": "^2.6.2",
|
||||||
|
"prettier-plugin-organize-imports": "^3.2.2",
|
||||||
|
"prettier-plugin-packagejson": "2.4.3",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"openinula": "0.1.1",
|
||||||
|
"serve-static": "^1.16.2"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
"chenxiaocong <xiaohuoni@gmail.com> (https://github.com/xiaohuoni)"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"father": "^4.5.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './dist/pluginUtils';
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = require('./dist/pluginUtils');
|
|
@ -0,0 +1,13 @@
|
||||||
|
module.exports = {
|
||||||
|
printWidth: 80,
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: 'all',
|
||||||
|
proseWrap: 'never',
|
||||||
|
endOfLine: 'lf',
|
||||||
|
overrides: [{ files: '.prettierrc', options: { parser: 'json' } }],
|
||||||
|
plugins: [
|
||||||
|
require.resolve('prettier-plugin-packagejson'),
|
||||||
|
require.resolve('prettier-plugin-organize-imports'),
|
||||||
|
],
|
||||||
|
pluginSearchDirs: false,
|
||||||
|
};
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { deepmerge, logger, yParser } from '@umijs/utils';
|
||||||
|
import { BUILD_COMMANDS, DEV_COMMAND } from './constants';
|
||||||
|
import {
|
||||||
|
checkLocal,
|
||||||
|
checkVersion as checkNodeVersion,
|
||||||
|
setNoDeprecation,
|
||||||
|
setNodeTitle,
|
||||||
|
} from './node';
|
||||||
|
import { Service } from './service';
|
||||||
|
|
||||||
|
interface IOpts {
|
||||||
|
args?: yParser.Arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function run(_opts?: IOpts) {
|
||||||
|
checkNodeVersion();
|
||||||
|
checkLocal();
|
||||||
|
setNodeTitle();
|
||||||
|
setNoDeprecation();
|
||||||
|
|
||||||
|
const args =
|
||||||
|
_opts?.args ||
|
||||||
|
yParser(process.argv.slice(2), {
|
||||||
|
alias: {
|
||||||
|
version: ['v'],
|
||||||
|
help: ['h'],
|
||||||
|
},
|
||||||
|
boolean: ['version'],
|
||||||
|
});
|
||||||
|
const command = args._[0];
|
||||||
|
|
||||||
|
if (command === DEV_COMMAND) {
|
||||||
|
process.env.NODE_ENV = 'development';
|
||||||
|
} else if (BUILD_COMMANDS.includes(command)) {
|
||||||
|
process.env.NODE_ENV = 'production';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const service = new Service();
|
||||||
|
|
||||||
|
await service.run2({
|
||||||
|
name: command,
|
||||||
|
args: deepmerge({}, args),
|
||||||
|
});
|
||||||
|
|
||||||
|
// handle restart for dev command
|
||||||
|
if (command === DEV_COMMAND) {
|
||||||
|
async function listener(data: any) {
|
||||||
|
if (data?.type === 'RESTART') {
|
||||||
|
// off self
|
||||||
|
process.off('message', listener);
|
||||||
|
|
||||||
|
// restart
|
||||||
|
run({ args });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('message', listener);
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logger.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
import { ApplyPluginsType, PluginManager } from './plugin';
|
||||||
|
|
||||||
|
const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));
|
||||||
|
|
||||||
|
test('PluginManager#applyPlugins in async=false mode', async () => {
|
||||||
|
const pm = new PluginManager({
|
||||||
|
validKeys: ['foo'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const asyncCall = jest.fn();
|
||||||
|
const syncCall = jest.fn();
|
||||||
|
|
||||||
|
pm.register({
|
||||||
|
apply: {
|
||||||
|
foo: async () => {
|
||||||
|
await delay(100);
|
||||||
|
asyncCall();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
path: '/a',
|
||||||
|
});
|
||||||
|
pm.register({
|
||||||
|
apply: {
|
||||||
|
foo: syncCall,
|
||||||
|
},
|
||||||
|
path: '/a',
|
||||||
|
});
|
||||||
|
|
||||||
|
await pm.applyPlugins({
|
||||||
|
key: 'foo',
|
||||||
|
type: ApplyPluginsType.event,
|
||||||
|
async: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(syncCall).toBeCalled();
|
||||||
|
expect(asyncCall).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PluginManager#applyPlugins in async=true mode', async () => {
|
||||||
|
const pm = new PluginManager({
|
||||||
|
validKeys: ['foo'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const asyncCall = jest.fn();
|
||||||
|
const syncCall = jest.fn();
|
||||||
|
|
||||||
|
pm.register({
|
||||||
|
apply: {
|
||||||
|
foo: async () => {
|
||||||
|
await delay(100);
|
||||||
|
asyncCall();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
path: '/a',
|
||||||
|
});
|
||||||
|
pm.register({
|
||||||
|
apply: {
|
||||||
|
foo: syncCall,
|
||||||
|
},
|
||||||
|
path: '/a',
|
||||||
|
});
|
||||||
|
|
||||||
|
await pm.applyPlugins({
|
||||||
|
key: 'foo',
|
||||||
|
type: ApplyPluginsType.event,
|
||||||
|
async: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(syncCall).toBeCalled();
|
||||||
|
expect(asyncCall).toBeCalled();
|
||||||
|
});
|
|
@ -0,0 +1,168 @@
|
||||||
|
import { assert, compose, isPromiseLike } from './utils';
|
||||||
|
|
||||||
|
export enum ApplyPluginsType {
|
||||||
|
compose = 'compose',
|
||||||
|
modify = 'modify',
|
||||||
|
event = 'event',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPlugin {
|
||||||
|
path?: string;
|
||||||
|
apply: Record<string, any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PluginManager {
|
||||||
|
opts: { validKeys: string[] };
|
||||||
|
hooks: {
|
||||||
|
[key: string]: any;
|
||||||
|
} = {};
|
||||||
|
constructor(opts: { validKeys: string[] }) {
|
||||||
|
this.opts = opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
register(plugin: IPlugin) {
|
||||||
|
assert(plugin.apply, `plugin register failed, apply must supplied`);
|
||||||
|
Object.keys(plugin.apply).forEach((key) => {
|
||||||
|
assert(
|
||||||
|
this.opts.validKeys.indexOf(key) > -1,
|
||||||
|
`register failed, invalid key ${key} ${
|
||||||
|
plugin.path ? `from plugin ${plugin.path}` : ''
|
||||||
|
}.`,
|
||||||
|
);
|
||||||
|
this.hooks[key] = (this.hooks[key] || []).concat(plugin.apply[key]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getHooks(keyWithDot: string) {
|
||||||
|
const [key, ...memberKeys] = keyWithDot.split('.');
|
||||||
|
let hooks = this.hooks[key] || [];
|
||||||
|
if (memberKeys.length) {
|
||||||
|
hooks = hooks
|
||||||
|
.map((hook: any) => {
|
||||||
|
try {
|
||||||
|
let ret = hook;
|
||||||
|
for (const memberKey of memberKeys) {
|
||||||
|
ret = ret[memberKey];
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
return hooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyPlugins({
|
||||||
|
key,
|
||||||
|
type,
|
||||||
|
initialValue,
|
||||||
|
args,
|
||||||
|
async,
|
||||||
|
}: {
|
||||||
|
key: string;
|
||||||
|
type: ApplyPluginsType;
|
||||||
|
initialValue?: any;
|
||||||
|
args?: object;
|
||||||
|
async?: boolean;
|
||||||
|
}) {
|
||||||
|
const hooks = this.getHooks(key) || [];
|
||||||
|
|
||||||
|
if (args) {
|
||||||
|
assert(
|
||||||
|
typeof args === 'object',
|
||||||
|
`applyPlugins failed, args must be plain object.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (async) {
|
||||||
|
assert(
|
||||||
|
type === ApplyPluginsType.modify || type === ApplyPluginsType.event,
|
||||||
|
`async only works with modify and event type.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case ApplyPluginsType.modify:
|
||||||
|
if (async) {
|
||||||
|
return hooks.reduce(
|
||||||
|
async (memo: any, hook: Function | Promise<any> | object) => {
|
||||||
|
assert(
|
||||||
|
typeof hook === 'function' ||
|
||||||
|
typeof hook === 'object' ||
|
||||||
|
isPromiseLike(hook),
|
||||||
|
`applyPlugins failed, all hooks for key ${key} must be function, plain object or Promise.`,
|
||||||
|
);
|
||||||
|
if (isPromiseLike(memo)) {
|
||||||
|
memo = await memo;
|
||||||
|
}
|
||||||
|
if (typeof hook === 'function') {
|
||||||
|
const ret = hook(memo, args);
|
||||||
|
if (isPromiseLike(ret)) {
|
||||||
|
return await ret;
|
||||||
|
} else {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isPromiseLike(hook)) {
|
||||||
|
hook = await hook;
|
||||||
|
}
|
||||||
|
return { ...memo, ...hook };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isPromiseLike(initialValue)
|
||||||
|
? initialValue
|
||||||
|
: Promise.resolve(initialValue),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return hooks.reduce((memo: any, hook: Function | object) => {
|
||||||
|
assert(
|
||||||
|
typeof hook === 'function' || typeof hook === 'object',
|
||||||
|
`applyPlugins failed, all hooks for key ${key} must be function or plain object.`,
|
||||||
|
);
|
||||||
|
if (typeof hook === 'function') {
|
||||||
|
return hook(memo, args);
|
||||||
|
} else {
|
||||||
|
// TODO: deepmerge?
|
||||||
|
return { ...memo, ...hook };
|
||||||
|
}
|
||||||
|
}, initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
case ApplyPluginsType.event:
|
||||||
|
return (async () => {
|
||||||
|
for (const hook of hooks) {
|
||||||
|
assert(
|
||||||
|
typeof hook === 'function',
|
||||||
|
`applyPlugins failed, all hooks for key ${key} must be function.`,
|
||||||
|
);
|
||||||
|
const ret = hook(args);
|
||||||
|
if (async && isPromiseLike(ret)) {
|
||||||
|
await ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
case ApplyPluginsType.compose:
|
||||||
|
return () => {
|
||||||
|
return compose({
|
||||||
|
fns: hooks.concat(initialValue),
|
||||||
|
args,
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(opts: { validKeys: string[]; plugins: IPlugin[] }) {
|
||||||
|
const pluginManager = new PluginManager({
|
||||||
|
validKeys: opts.validKeys,
|
||||||
|
});
|
||||||
|
opts.plugins.forEach((plugin) => {
|
||||||
|
pluginManager.register(plugin);
|
||||||
|
});
|
||||||
|
return pluginManager;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// plugins meta info (in tmp file)
|
||||||
|
// hooks api: usePlugin
|
|
@ -0,0 +1,21 @@
|
||||||
|
export function assert(value: unknown, message: string) {
|
||||||
|
if (!value) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compose({
|
||||||
|
fns,
|
||||||
|
args,
|
||||||
|
}: {
|
||||||
|
fns: (Function | any)[];
|
||||||
|
args?: object;
|
||||||
|
}) {
|
||||||
|
if (fns.length === 1) {
|
||||||
|
return fns[0];
|
||||||
|
}
|
||||||
|
const last = fns.pop();
|
||||||
|
return fns.reduce((a, b) => () => b(a, args), last);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPromiseLike(obj: any) {
|
||||||
|
return !!obj && typeof obj === 'object' && typeof obj.then === 'function';
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { IApi } from '@aluni/preset-inula';
|
||||||
|
import { logger } from '@umijs/utils';
|
||||||
|
import { sync } from '@umijs/utils/compiled/cross-spawn';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
const CONFIG_FILES = ['.prettierrc', '.prettierrc.js'];
|
||||||
|
|
||||||
|
function getConfigFiles(p: string): string[] | undefined {
|
||||||
|
return CONFIG_FILES.filter((f) => existsSync(join(p, f)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
api.registerCommand({
|
||||||
|
name: 'format',
|
||||||
|
alias: 'prettier',
|
||||||
|
description: 'prettier --write .',
|
||||||
|
configResolveMode: 'loose',
|
||||||
|
fn({ args }) {
|
||||||
|
let defaultPrettierConfig = join(__dirname, '..', '..', 'prettier.js');
|
||||||
|
const configFiles = getConfigFiles(api.paths.absSrcPath);
|
||||||
|
if (configFiles && configFiles[0]) {
|
||||||
|
defaultPrettierConfig = configFiles[0];
|
||||||
|
}
|
||||||
|
logger.info(`prettier config`, defaultPrettierConfig);
|
||||||
|
const prettier = join(
|
||||||
|
dirname(require.resolve('prettier/package.json')),
|
||||||
|
'bin-prettier',
|
||||||
|
);
|
||||||
|
const spawn = sync(
|
||||||
|
'node',
|
||||||
|
[
|
||||||
|
prettier,
|
||||||
|
`--config ${defaultPrettierConfig}`,
|
||||||
|
`--write ${api.cwd}`,
|
||||||
|
...args._,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
env: process.env,
|
||||||
|
cwd: process.cwd(),
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (spawn.status !== 0) {
|
||||||
|
console.log(`prettier-scripts run fail`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { IApi } from '@aluni/preset-inula';
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
api.modifyConfig((memo: any) => {
|
||||||
|
memo.block = {
|
||||||
|
defaultGitUrl: 'https://github.com/ant-design/pro-blocks',
|
||||||
|
npmClient: 'pnpm',
|
||||||
|
closeFastGithub: true,
|
||||||
|
homedir: false,
|
||||||
|
useUI: true,
|
||||||
|
...memo.block,
|
||||||
|
};
|
||||||
|
// mock 增加
|
||||||
|
memo.mock = {
|
||||||
|
include: ['src/pages/**/_mock.ts'],
|
||||||
|
};
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
// block 提供的 api
|
||||||
|
// @ts-ignore
|
||||||
|
api?._modifyBlockFile?.((memo) => {
|
||||||
|
// TODO: block 生成 还有什么操作,都可以在这里处理
|
||||||
|
return memo.replaceAll('request', 'ir');
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,11 @@
|
||||||
|
export const MIN_NODE_VERSION = 14;
|
||||||
|
export const DEFAULT_CONFIG_FILES = [
|
||||||
|
'.inularc.ts',
|
||||||
|
'.inularc.js',
|
||||||
|
'config/config.ts',
|
||||||
|
'config/config.js',
|
||||||
|
];
|
||||||
|
export const FRAMEWORK_NAME = 'inula-max';
|
||||||
|
export const WATCH_DEBOUNCE_STEP = 300;
|
||||||
|
export const DEV_COMMAND = 'dev';
|
||||||
|
export const BUILD_COMMANDS = ['build', 'prebundle'];
|
|
@ -0,0 +1,13 @@
|
||||||
|
// @ts-ignore
|
||||||
|
import { IConfigFromPlugins } from '@@/core/pluginConfig';
|
||||||
|
import type { IConfig } from './plugin/preset-inula/src';
|
||||||
|
|
||||||
|
type ConfigType = IConfigFromPlugins & IConfig;
|
||||||
|
/**
|
||||||
|
* 通过方法的方式配置umi,能带来更好的 typescript 体验
|
||||||
|
* @param {ConfigType} config
|
||||||
|
* @returns ConfigType
|
||||||
|
*/
|
||||||
|
export function defineConfig(config: ConfigType): ConfigType {
|
||||||
|
return config;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { IServicePluginAPI, PluginAPI } from '@umijs/core';
|
||||||
|
|
||||||
|
export { run } from './cli';
|
||||||
|
export { defineConfig } from './defineConfig';
|
||||||
|
export * from './service';
|
||||||
|
export type IApi = PluginAPI & IServicePluginAPI;
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { logger } from '@umijs/utils';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { FRAMEWORK_NAME, MIN_NODE_VERSION } from './constants';
|
||||||
|
|
||||||
|
export function checkVersion() {
|
||||||
|
const v = parseInt(process.version.slice(1));
|
||||||
|
if (v < MIN_NODE_VERSION || v === 15 || v === 17) {
|
||||||
|
logger.error(
|
||||||
|
`Your node version ${v} is not supported, please upgrade to ${MIN_NODE_VERSION} or above except 15 or 17.`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkLocal() {
|
||||||
|
if (existsSync(join(__dirname, '../../jest.config.ts'))) {
|
||||||
|
logger.info('@local');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNodeTitle(name?: string) {
|
||||||
|
if (process.title === 'node') {
|
||||||
|
process.title = name || FRAMEWORK_NAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNoDeprecation() {
|
||||||
|
// Use magic to suppress node deprecation warnings
|
||||||
|
// See: https://github.com/nodejs/node/blob/master/lib/internal/process/warning.js#L77
|
||||||
|
// @ts-ignore
|
||||||
|
process.noDeprecation = '1';
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
// 只导出了 utils 的方法,如果有用到 bundler-utils 再增加
|
||||||
|
export * from '@umijs/utils';
|
|
@ -0,0 +1,173 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { fsExtra, resolve } from '@umijs/utils';
|
||||||
|
import { existsSync, writeFileSync } from 'fs';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { withTmpPath } from './withTmpPath';
|
||||||
|
|
||||||
|
export function resolveProjectDep(opts: {
|
||||||
|
pkg: any;
|
||||||
|
cwd: string;
|
||||||
|
dep: string;
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
opts.pkg.dependencies?.[opts.dep] ||
|
||||||
|
opts.pkg.devDependencies?.[opts.dep]
|
||||||
|
) {
|
||||||
|
return dirname(
|
||||||
|
resolve.sync(`${opts.dep}/package.json`, {
|
||||||
|
basedir: opts.cwd,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default (api: IApi) => {
|
||||||
|
const defaultTmpPath = withTmpPath({ api, path: 'Layout.tsx' });
|
||||||
|
api.describe({
|
||||||
|
key: 'proLayout',
|
||||||
|
config: {
|
||||||
|
schema({ zod }) {
|
||||||
|
return zod
|
||||||
|
.object({
|
||||||
|
// 可以把文件写到项目中
|
||||||
|
tmpPath: zod.string(),
|
||||||
|
// 当 layout 文件存在时,不更新,用户可以保留自己的修改
|
||||||
|
reWriteTmp: zod.boolean(),
|
||||||
|
})
|
||||||
|
.deepPartial();
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
tmpPath: defaultTmpPath,
|
||||||
|
reWriteTmp: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableBy({ userConfig }) {
|
||||||
|
// 使用这个插件的,必须开启 antd 插件
|
||||||
|
return userConfig.antd && userConfig.proLayout;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
api.addRuntimePluginKey(() => ['proLayout']);
|
||||||
|
|
||||||
|
let pkgPath: string;
|
||||||
|
try {
|
||||||
|
pkgPath =
|
||||||
|
resolveProjectDep({
|
||||||
|
pkg: api.pkg,
|
||||||
|
cwd: api.cwd,
|
||||||
|
dep: '@ant-design/pro-components',
|
||||||
|
}) || dirname(require.resolve('@ant-design/pro-components/package.json'));
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
api.modifyTSConfig((memo) => {
|
||||||
|
memo.compilerOptions.paths['@ant-design/pro-components'] = [pkgPath];
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.modifyConfig((memo) => {
|
||||||
|
memo.alias['@ant-design/pro-components'] = pkgPath;
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onGenerateFiles(() => {
|
||||||
|
const tmpPath = api.config.proLayout.tmpPath || defaultTmpPath;
|
||||||
|
if (api.config.proLayout.reWriteTmp === false && existsSync(tmpPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
fsExtra.mkdirpSync(dirname(tmpPath));
|
||||||
|
writeFileSync(
|
||||||
|
tmpPath,
|
||||||
|
`import { getPluginManager } from '@@/core/plugin';
|
||||||
|
import type { ProSettings } from '@ant-design/pro-components';
|
||||||
|
import {
|
||||||
|
PageContainer as _PageContainer,
|
||||||
|
ProCard as _ProCard,
|
||||||
|
ProConfigProvider as _ProConfigProvider,
|
||||||
|
ProLayout as _ProLayout,
|
||||||
|
SettingDrawer,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { ConfigProvider as _ConfigProvider } from 'antd';
|
||||||
|
import { Fragment, useLocation, useOutlet, useState } from 'inula';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const proConfig = getPluginManager().applyPlugins({
|
||||||
|
key: 'proLayout',
|
||||||
|
type: 'modify',
|
||||||
|
initialValue: {},
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
proConfigProvider,
|
||||||
|
configProvider,
|
||||||
|
root = {
|
||||||
|
id: 'inula-pro-layout',
|
||||||
|
style: {
|
||||||
|
height: '100vh',
|
||||||
|
overflow: 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
proLayout,
|
||||||
|
pageContainer,
|
||||||
|
proCard,
|
||||||
|
settingDrawer = {
|
||||||
|
layout: 'top',
|
||||||
|
},
|
||||||
|
} = proConfig;
|
||||||
|
const ProConfigProvider = !!proConfigProvider ? _ProConfigProvider : Fragment;
|
||||||
|
const ConfigProvider = !!configProvider ? _ConfigProvider : Fragment;
|
||||||
|
const ProLayout = !!proLayout ? _ProLayout : Fragment;
|
||||||
|
const PageContainer = !!pageContainer ? _PageContainer : Fragment;
|
||||||
|
const ProCard = !!proCard ? _ProCard : Fragment;
|
||||||
|
const [settings, setSetting] = useState<Partial<ProSettings> | undefined>(
|
||||||
|
settingDrawer,
|
||||||
|
);
|
||||||
|
const outlet = useOutlet();
|
||||||
|
const location = useLocation();
|
||||||
|
const { pathname } = location;
|
||||||
|
if (typeof document === 'undefined') {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div {...root}>
|
||||||
|
<ProConfigProvider {...(proConfigProvider || {})}>
|
||||||
|
<ConfigProvider {...(configProvider || {})}>
|
||||||
|
<ProLayout
|
||||||
|
location={{
|
||||||
|
pathname,
|
||||||
|
}}
|
||||||
|
{...settings}
|
||||||
|
{...(proLayout || {})}
|
||||||
|
>
|
||||||
|
<PageContainer {...(pageContainer || {})}>
|
||||||
|
<ProCard {...(proCard || {})}>{outlet}</ProCard>
|
||||||
|
</PageContainer>
|
||||||
|
<SettingDrawer
|
||||||
|
pathname={pathname}
|
||||||
|
enableDarkTheme
|
||||||
|
hideHintAlert
|
||||||
|
getContainer={(e: any) => {
|
||||||
|
if (typeof window === 'undefined') return e;
|
||||||
|
return document.getElementById('inula-pro-layout');
|
||||||
|
}}
|
||||||
|
settings={settings}
|
||||||
|
onSettingChange={(changeSetting) => {
|
||||||
|
setSetting(changeSetting);
|
||||||
|
}}
|
||||||
|
disableUrlParams={false}
|
||||||
|
/>
|
||||||
|
</ProLayout>
|
||||||
|
</ConfigProvider>
|
||||||
|
</ProConfigProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
api.addLayouts(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'ant-design-pro-layout',
|
||||||
|
file: api.config.proLayout.tmpPath || defaultTmpPath,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { winPath } from '@umijs/utils';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export function withTmpPath(opts: {
|
||||||
|
api: IApi;
|
||||||
|
path: string;
|
||||||
|
noPluginDir?: boolean;
|
||||||
|
}) {
|
||||||
|
return winPath(
|
||||||
|
join(
|
||||||
|
opts.api.paths.absTmpPath,
|
||||||
|
opts.api.plugin.key && !opts.noPluginDir
|
||||||
|
? `plugin-${opts.api.plugin.key}`
|
||||||
|
: '',
|
||||||
|
opts.path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export const TEMPLATES_DIR = join(__dirname, '../templates');
|
|
@ -0,0 +1,91 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { resolve } from '@umijs/utils';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
export function resolveProjectDep(opts: {
|
||||||
|
pkg: any;
|
||||||
|
cwd: string;
|
||||||
|
dep: string;
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
opts.pkg.dependencies?.[opts.dep] ||
|
||||||
|
opts.pkg.devDependencies?.[opts.dep]
|
||||||
|
) {
|
||||||
|
return dirname(
|
||||||
|
resolve.sync(`${opts.dep}/package.json`, {
|
||||||
|
basedir: opts.cwd,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
let antdPath: string;
|
||||||
|
let iconsPath: string;
|
||||||
|
let emotionPath: string;
|
||||||
|
try {
|
||||||
|
antdPath =
|
||||||
|
resolveProjectDep({
|
||||||
|
pkg: api.pkg,
|
||||||
|
cwd: api.cwd,
|
||||||
|
dep: 'antd',
|
||||||
|
}) || dirname(require.resolve('antd/package.json'));
|
||||||
|
iconsPath =
|
||||||
|
resolveProjectDep({
|
||||||
|
pkg: api.pkg,
|
||||||
|
cwd: api.cwd,
|
||||||
|
dep: '@ant-design/icons',
|
||||||
|
}) || dirname(require.resolve('@ant-design/icons/package.json'));
|
||||||
|
emotionPath =
|
||||||
|
resolveProjectDep({
|
||||||
|
pkg: api.pkg,
|
||||||
|
cwd: api.cwd,
|
||||||
|
dep: '@emotion/css',
|
||||||
|
}) || dirname(require.resolve('@emotion/css/package.json'));
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
api.describe({
|
||||||
|
key: 'antd',
|
||||||
|
config: {
|
||||||
|
schema({ zod }) {
|
||||||
|
return zod.object({}).deepPartial();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableBy({ userConfig }) {
|
||||||
|
// 由于本插件有 api.modifyConfig 的调用,以及 Umi 框架的限制
|
||||||
|
// 在其他插件中通过 api.modifyDefaultConfig 设置 antd 并不能让 api.modifyConfig 生效
|
||||||
|
// 所以这里通过环境变量来判断是否启用
|
||||||
|
return process.env.UMI_PLUGIN_ANTD_ENABLE || userConfig.antd;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.modifyAppData((memo) => {
|
||||||
|
const version = require(`${antdPath}/package.json`).version;
|
||||||
|
memo.antd = {
|
||||||
|
antdPath,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.modifyTSConfig((memo) => {
|
||||||
|
memo.compilerOptions.paths.antd = [antdPath];
|
||||||
|
memo.compilerOptions.paths['@ant-design/icons'] = [iconsPath];
|
||||||
|
memo.compilerOptions.paths['@emotion/css'] = [emotionPath];
|
||||||
|
memo.compilerOptions.paths['inula/antd'] = [antdPath];
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.modifyConfig((memo) => {
|
||||||
|
memo.alias.antd = antdPath;
|
||||||
|
memo.alias['@ant-design/icons'] = iconsPath;
|
||||||
|
memo.alias['@emotion/css'] = emotionPath;
|
||||||
|
memo.alias = {
|
||||||
|
'inula/antd': antdPath,
|
||||||
|
...memo.alias,
|
||||||
|
};
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onGenerateFiles(() => {});
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { winPath } from '@umijs/utils';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export function withTmpPath(opts: {
|
||||||
|
api: IApi;
|
||||||
|
path: string;
|
||||||
|
noPluginDir?: boolean;
|
||||||
|
}) {
|
||||||
|
return winPath(
|
||||||
|
join(
|
||||||
|
opts.api.paths.absTmpPath,
|
||||||
|
opts.api.plugin.key && !opts.noPluginDir
|
||||||
|
? `plugin-${opts.api.plugin.key}`
|
||||||
|
: '',
|
||||||
|
opts.path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,386 @@
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export const TEMPLATES_DIR = join(__dirname, '../templates');
|
||||||
|
|
||||||
|
export const LangCnLabel: any = {
|
||||||
|
'ar-EG': '埃及阿拉伯语',
|
||||||
|
'az-AZ': '阿塞拜疆语',
|
||||||
|
'bg-BG': '保加利亚语',
|
||||||
|
'bn-BD': '孟加拉语',
|
||||||
|
'ca-ES': '加泰罗尼亚语',
|
||||||
|
'cs-CZ': '捷克语',
|
||||||
|
'da-DK': '丹麦语',
|
||||||
|
'de-DE': '德语',
|
||||||
|
'el-GR': '希腊语',
|
||||||
|
'en-GB': '英语(英国)',
|
||||||
|
'en-US': '英语(美国)',
|
||||||
|
'es-ES': '西班牙语',
|
||||||
|
'et-EE': '爱沙尼亚语',
|
||||||
|
'fa-IR': '伊朗波斯语',
|
||||||
|
'fi-FI': '芬兰语',
|
||||||
|
'fr-BE': '法语(比利时)',
|
||||||
|
'fr-FR': '法语',
|
||||||
|
'ga-IE': '爱尔兰语',
|
||||||
|
'he-IL': '希伯来语',
|
||||||
|
'hi-IN': '印地语',
|
||||||
|
'hr-HR': '克罗地亚语',
|
||||||
|
'hu-HU': '匈牙利语',
|
||||||
|
'hy-AM': '亚美尼亚语',
|
||||||
|
'id-ID': '印度尼西亚语',
|
||||||
|
'it-IT': '意大利语',
|
||||||
|
'is-IS': '冰岛语',
|
||||||
|
'ja-JP': '日语',
|
||||||
|
'ku-IQ': '库尔德语',
|
||||||
|
'kn-IN': '卡纳达语',
|
||||||
|
'ko-KR': '韩语',
|
||||||
|
'lv-LV': '拉脱维亚语',
|
||||||
|
'mk-MK': '马其顿语',
|
||||||
|
'mn-MN': '蒙古语',
|
||||||
|
'ms-MY': '马来语',
|
||||||
|
'nb-NO': '挪威语',
|
||||||
|
'ne-NP': '尼泊尔语',
|
||||||
|
'nl-BE': '荷兰语(比利时)',
|
||||||
|
'nl-NL': '荷兰语',
|
||||||
|
'pl-PL': '波兰语',
|
||||||
|
'pt-BR': '葡萄牙语(巴西)',
|
||||||
|
'pt-PT': '葡萄牙语',
|
||||||
|
'ro-RO': '罗马尼亚语',
|
||||||
|
'ru-RU': '俄语',
|
||||||
|
'sk-SK': '斯洛伐克语',
|
||||||
|
'sr-RS': '塞尔维亚语',
|
||||||
|
'sl-SI': '斯洛文尼亚语',
|
||||||
|
'sv-SE': '瑞典语',
|
||||||
|
'ta-IN': '泰米尔语',
|
||||||
|
'th-TH': '泰语',
|
||||||
|
'tr-TR': '土耳其语',
|
||||||
|
'uk-UA': '乌克兰语',
|
||||||
|
'vi-VN': '越南语',
|
||||||
|
'zh-CN': '简体中文',
|
||||||
|
'zh-TW': '繁体中文',
|
||||||
|
};
|
||||||
|
export const DefaultLangUConfigMap: any = {
|
||||||
|
'ar-EG': {
|
||||||
|
lang: 'ar-EG',
|
||||||
|
label: 'العربية',
|
||||||
|
icon: '🇪🇬',
|
||||||
|
title: 'لغة',
|
||||||
|
},
|
||||||
|
'az-AZ': {
|
||||||
|
lang: 'az-AZ',
|
||||||
|
label: 'Azərbaycan dili',
|
||||||
|
icon: '🇦🇿',
|
||||||
|
title: 'Dil',
|
||||||
|
},
|
||||||
|
'bg-BG': {
|
||||||
|
lang: 'bg-BG',
|
||||||
|
label: 'Български език',
|
||||||
|
icon: '🇧🇬',
|
||||||
|
title: 'език',
|
||||||
|
},
|
||||||
|
'bn-BD': {
|
||||||
|
lang: 'bn-BD',
|
||||||
|
label: 'বাংলা',
|
||||||
|
icon: '🇧🇩',
|
||||||
|
title: 'ভাষা',
|
||||||
|
},
|
||||||
|
'ca-ES': {
|
||||||
|
lang: 'ca-ES',
|
||||||
|
label: 'Catalá',
|
||||||
|
icon: '🇨🇦',
|
||||||
|
title: 'llengua',
|
||||||
|
},
|
||||||
|
'cs-CZ': {
|
||||||
|
lang: 'cs-CZ',
|
||||||
|
label: 'Čeština',
|
||||||
|
icon: '🇨🇿',
|
||||||
|
title: 'Jazyk',
|
||||||
|
},
|
||||||
|
'da-DK': {
|
||||||
|
lang: 'da-DK',
|
||||||
|
label: 'Dansk',
|
||||||
|
icon: '🇩🇰',
|
||||||
|
title: 'Sprog',
|
||||||
|
},
|
||||||
|
'de-DE': {
|
||||||
|
lang: 'de-DE',
|
||||||
|
label: 'Deutsch',
|
||||||
|
icon: '🇩🇪',
|
||||||
|
title: 'Sprache',
|
||||||
|
},
|
||||||
|
'el-GR': {
|
||||||
|
lang: 'el-GR',
|
||||||
|
label: 'Ελληνικά',
|
||||||
|
icon: '🇬🇷',
|
||||||
|
title: 'Γλώσσα',
|
||||||
|
},
|
||||||
|
'en-GB': {
|
||||||
|
lang: 'en-GB',
|
||||||
|
label: 'English',
|
||||||
|
icon: '🇬🇧',
|
||||||
|
title: 'Language',
|
||||||
|
},
|
||||||
|
'en-US': {
|
||||||
|
lang: 'en-US',
|
||||||
|
label: 'English',
|
||||||
|
icon: '🇺🇸',
|
||||||
|
title: 'Language',
|
||||||
|
},
|
||||||
|
'es-ES': {
|
||||||
|
lang: 'es-ES',
|
||||||
|
label: 'Español',
|
||||||
|
icon: '🇪🇸',
|
||||||
|
title: 'Idioma',
|
||||||
|
},
|
||||||
|
'et-EE': {
|
||||||
|
lang: 'et-EE',
|
||||||
|
label: 'Eesti',
|
||||||
|
icon: '🇪🇪',
|
||||||
|
title: 'Keel',
|
||||||
|
},
|
||||||
|
'fa-IR': {
|
||||||
|
lang: 'fa-IR',
|
||||||
|
label: 'فارسی',
|
||||||
|
icon: '🇮🇷',
|
||||||
|
title: 'زبان',
|
||||||
|
},
|
||||||
|
'fi-FI': {
|
||||||
|
lang: 'fi-FI',
|
||||||
|
label: 'Suomi',
|
||||||
|
icon: '🇫🇮',
|
||||||
|
title: 'Kieli',
|
||||||
|
},
|
||||||
|
'fr-BE': {
|
||||||
|
lang: 'fr-BE',
|
||||||
|
label: 'Français',
|
||||||
|
icon: '🇧🇪',
|
||||||
|
title: 'Langue',
|
||||||
|
},
|
||||||
|
'fr-FR': {
|
||||||
|
lang: 'fr-FR',
|
||||||
|
label: 'Français',
|
||||||
|
icon: '🇫🇷',
|
||||||
|
title: 'Langue',
|
||||||
|
},
|
||||||
|
'ga-IE': {
|
||||||
|
lang: 'ga-IE',
|
||||||
|
label: 'Gaeilge',
|
||||||
|
icon: '🇮🇪',
|
||||||
|
title: 'Teanga',
|
||||||
|
},
|
||||||
|
'he-IL': {
|
||||||
|
lang: 'he-IL',
|
||||||
|
label: 'עברית',
|
||||||
|
icon: '🇮🇱',
|
||||||
|
title: 'שפה',
|
||||||
|
},
|
||||||
|
'hi-IN': {
|
||||||
|
lang: 'hi-IN',
|
||||||
|
label: 'हिन्दी, हिंदी',
|
||||||
|
icon: '🇮🇳',
|
||||||
|
title: 'भाषा: हिन्दी',
|
||||||
|
},
|
||||||
|
'hr-HR': {
|
||||||
|
lang: 'hr-HR',
|
||||||
|
label: 'Hrvatski jezik',
|
||||||
|
icon: '🇭🇷',
|
||||||
|
title: 'Jezik',
|
||||||
|
},
|
||||||
|
'hu-HU': {
|
||||||
|
lang: 'hu-HU',
|
||||||
|
label: 'Magyar',
|
||||||
|
icon: '🇭🇺',
|
||||||
|
title: 'Nyelv',
|
||||||
|
},
|
||||||
|
'hy-AM': {
|
||||||
|
lang: 'hu-HU',
|
||||||
|
label: 'Հայերեն',
|
||||||
|
icon: '🇦🇲',
|
||||||
|
title: 'Լեզու',
|
||||||
|
},
|
||||||
|
'id-ID': {
|
||||||
|
lang: 'id-ID',
|
||||||
|
label: 'Bahasa Indonesia',
|
||||||
|
icon: '🇮🇩',
|
||||||
|
title: 'Bahasa',
|
||||||
|
},
|
||||||
|
'it-IT': {
|
||||||
|
lang: 'it-IT',
|
||||||
|
label: 'Italiano',
|
||||||
|
icon: '🇮🇹',
|
||||||
|
title: 'Linguaggio',
|
||||||
|
},
|
||||||
|
'is-IS': {
|
||||||
|
lang: 'is-IS',
|
||||||
|
label: 'Íslenska',
|
||||||
|
icon: '🇮🇸',
|
||||||
|
title: 'Tungumál',
|
||||||
|
},
|
||||||
|
'ja-JP': {
|
||||||
|
lang: 'ja-JP',
|
||||||
|
label: '日本語',
|
||||||
|
icon: '🇯🇵',
|
||||||
|
title: '言語',
|
||||||
|
},
|
||||||
|
'ku-IQ': {
|
||||||
|
lang: 'ku-IQ',
|
||||||
|
label: 'کوردی',
|
||||||
|
icon: '🇮🇶',
|
||||||
|
title: 'Ziman',
|
||||||
|
},
|
||||||
|
'kn-IN': {
|
||||||
|
lang: 'kn-IN',
|
||||||
|
label: 'ಕನ್ನಡ',
|
||||||
|
icon: '🇮🇳',
|
||||||
|
title: 'ಭಾಷೆ',
|
||||||
|
},
|
||||||
|
'ko-KR': {
|
||||||
|
lang: 'ko-KR',
|
||||||
|
label: '한국어',
|
||||||
|
icon: '🇰🇷',
|
||||||
|
title: '언어',
|
||||||
|
},
|
||||||
|
'lv-LV': {
|
||||||
|
lang: 'lv-LV',
|
||||||
|
label: 'Latviešu valoda',
|
||||||
|
icon: '🇱🇮',
|
||||||
|
title: 'Kalba',
|
||||||
|
},
|
||||||
|
'mk-MK': {
|
||||||
|
lang: 'mk-MK',
|
||||||
|
label: 'македонски јазик',
|
||||||
|
icon: '🇲🇰',
|
||||||
|
title: 'Јазик',
|
||||||
|
},
|
||||||
|
'mn-MN': {
|
||||||
|
lang: 'mn-MN',
|
||||||
|
label: 'Монгол хэл',
|
||||||
|
icon: '🇲🇳',
|
||||||
|
title: 'Хэл',
|
||||||
|
},
|
||||||
|
'ms-MY': {
|
||||||
|
lang: 'ms-MY',
|
||||||
|
label: 'بهاس ملايو',
|
||||||
|
icon: '🇲🇾',
|
||||||
|
title: 'Bahasa',
|
||||||
|
},
|
||||||
|
'nb-NO': {
|
||||||
|
lang: 'nb-NO',
|
||||||
|
label: 'Norsk',
|
||||||
|
icon: '🇳🇴',
|
||||||
|
title: 'Språk',
|
||||||
|
},
|
||||||
|
'ne-NP': {
|
||||||
|
lang: 'ne-NP',
|
||||||
|
label: 'नेपाली',
|
||||||
|
icon: '🇳🇵',
|
||||||
|
title: 'भाषा',
|
||||||
|
},
|
||||||
|
'nl-BE': {
|
||||||
|
lang: 'nl-BE',
|
||||||
|
label: 'Vlaams',
|
||||||
|
icon: '🇧🇪',
|
||||||
|
title: 'Taal',
|
||||||
|
},
|
||||||
|
'nl-NL': {
|
||||||
|
lang: 'nl-NL',
|
||||||
|
label: 'Nederlands',
|
||||||
|
icon: '🇳🇱',
|
||||||
|
title: 'Taal',
|
||||||
|
},
|
||||||
|
'pl-PL': {
|
||||||
|
lang: 'pl-PL',
|
||||||
|
label: 'Polski',
|
||||||
|
icon: '🇵🇱',
|
||||||
|
title: 'Język',
|
||||||
|
},
|
||||||
|
'pt-BR': {
|
||||||
|
lang: 'pt-BR',
|
||||||
|
label: 'Português',
|
||||||
|
icon: '🇧🇷',
|
||||||
|
title: 'Idiomas',
|
||||||
|
},
|
||||||
|
'pt-PT': {
|
||||||
|
lang: 'pt-PT',
|
||||||
|
label: 'Português',
|
||||||
|
icon: '🇵🇹',
|
||||||
|
title: 'Idiomas',
|
||||||
|
},
|
||||||
|
'ro-RO': {
|
||||||
|
lang: 'ro-RO',
|
||||||
|
label: 'Română',
|
||||||
|
icon: '🇷🇴',
|
||||||
|
title: 'Limba',
|
||||||
|
},
|
||||||
|
'ru-RU': {
|
||||||
|
lang: 'ru-RU',
|
||||||
|
label: 'Русский',
|
||||||
|
icon: '🇷🇺',
|
||||||
|
title: 'язык',
|
||||||
|
},
|
||||||
|
'sk-SK': {
|
||||||
|
lang: 'sk-SK',
|
||||||
|
label: 'Slovenčina',
|
||||||
|
icon: '🇸🇰',
|
||||||
|
title: 'Jazyk',
|
||||||
|
},
|
||||||
|
'sr-RS': {
|
||||||
|
lang: 'sr-RS',
|
||||||
|
label: 'српски језик',
|
||||||
|
icon: '🇸🇷',
|
||||||
|
title: 'Језик',
|
||||||
|
},
|
||||||
|
'sl-SI': {
|
||||||
|
lang: 'sl-SI',
|
||||||
|
label: 'Slovenščina',
|
||||||
|
icon: '🇸🇱',
|
||||||
|
title: 'Jezik',
|
||||||
|
},
|
||||||
|
'sv-SE': {
|
||||||
|
lang: 'sv-SE',
|
||||||
|
label: 'Svenska',
|
||||||
|
icon: '🇸🇪',
|
||||||
|
title: 'Språk',
|
||||||
|
},
|
||||||
|
'ta-IN': {
|
||||||
|
lang: 'ta-IN',
|
||||||
|
label: 'தமிழ்',
|
||||||
|
icon: '🇮🇳',
|
||||||
|
title: 'மொழி',
|
||||||
|
},
|
||||||
|
'th-TH': {
|
||||||
|
lang: 'th-TH',
|
||||||
|
label: 'ไทย',
|
||||||
|
icon: '🇹🇭',
|
||||||
|
title: 'ภาษา',
|
||||||
|
},
|
||||||
|
'tr-TR': {
|
||||||
|
lang: 'tr-TR',
|
||||||
|
label: 'Türkçe',
|
||||||
|
icon: '🇹🇷',
|
||||||
|
title: 'Dil',
|
||||||
|
},
|
||||||
|
'uk-UA': {
|
||||||
|
lang: 'uk-UA',
|
||||||
|
label: 'Українська',
|
||||||
|
icon: '🇺🇰',
|
||||||
|
title: 'Мова',
|
||||||
|
},
|
||||||
|
'vi-VN': {
|
||||||
|
lang: 'vi-VN',
|
||||||
|
label: 'Tiếng Việt',
|
||||||
|
icon: '🇻🇳',
|
||||||
|
title: 'Ngôn ngữ',
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
lang: 'zh-CN',
|
||||||
|
label: '简体中文',
|
||||||
|
icon: '🇨🇳',
|
||||||
|
title: '语言',
|
||||||
|
},
|
||||||
|
'zh-TW': {
|
||||||
|
lang: 'zh-TW',
|
||||||
|
label: '繁體中文',
|
||||||
|
icon: '🇭🇰',
|
||||||
|
title: '語言',
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,282 @@
|
||||||
|
import { IApi, IAzureSend } from '@aluni/types';
|
||||||
|
import esbuild from '@umijs/bundler-utils/compiled/esbuild';
|
||||||
|
import {
|
||||||
|
fsExtra,
|
||||||
|
logger,
|
||||||
|
Mustache,
|
||||||
|
prompts,
|
||||||
|
register,
|
||||||
|
resolve,
|
||||||
|
winPath,
|
||||||
|
} from '@umijs/utils';
|
||||||
|
import { existsSync, readFileSync } from 'fs';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { DefaultLangUConfigMap, LangCnLabel, TEMPLATES_DIR } from './constants';
|
||||||
|
import { getLocaleList, IGetLocaleFileListResult } from './localeUtils';
|
||||||
|
import { withTmpPath } from './withTmpPath';
|
||||||
|
|
||||||
|
export enum GeneratorType {
|
||||||
|
generate = 'generate',
|
||||||
|
enable = 'enable',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveProjectDep(opts: {
|
||||||
|
pkg: any;
|
||||||
|
cwd: string;
|
||||||
|
dep: string;
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
opts.pkg.dependencies?.[opts.dep] ||
|
||||||
|
opts.pkg.devDependencies?.[opts.dep]
|
||||||
|
) {
|
||||||
|
return dirname(
|
||||||
|
resolve.sync(`${opts.dep}/package.json`, {
|
||||||
|
basedir: opts.cwd,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
api.describe({
|
||||||
|
key: 'intl',
|
||||||
|
config: {
|
||||||
|
schema({ zod }) {
|
||||||
|
return zod
|
||||||
|
.object({
|
||||||
|
// 默认的 语言
|
||||||
|
default: zod.string(),
|
||||||
|
// 默认的文件路径
|
||||||
|
localeFolder: zod.string(),
|
||||||
|
})
|
||||||
|
.partial();
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
default: 'zh-CN',
|
||||||
|
localeFolder: 'locales',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const getList = async (): Promise<IGetLocaleFileListResult[]> => {
|
||||||
|
const { paths } = api;
|
||||||
|
return getLocaleList({
|
||||||
|
localeFolder: 'locales',
|
||||||
|
separator: api.config.intl?.baseSeparator,
|
||||||
|
absSrcPath: paths.absSrcPath,
|
||||||
|
absPagesPath: paths.absPagesPath,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
api.onGenerateFiles(async () => {
|
||||||
|
const intlPath =
|
||||||
|
resolveProjectDep({
|
||||||
|
pkg: api.pkg,
|
||||||
|
cwd: api.cwd,
|
||||||
|
dep: 'inula-intl',
|
||||||
|
}) || dirname(require.resolve('inula-intl/package.json'));
|
||||||
|
// intl.tsx
|
||||||
|
api.writeTmpFile({
|
||||||
|
path: 'intl.tsx',
|
||||||
|
tpl: `
|
||||||
|
import { getPluginManager } from '../core/plugin';
|
||||||
|
import {IntlProvider} from '${intlPath}';
|
||||||
|
import { localeInfo, getLocale, event, LANG_CHANGE_EVENT } from './localeExports';
|
||||||
|
import Inula from '${api.appData.openinula.path}';
|
||||||
|
const useIsomorphicLayoutEffect =
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
typeof window.document !== 'undefined' &&
|
||||||
|
typeof window.document.createElement !== 'undefined'
|
||||||
|
? Inula.useLayoutEffect
|
||||||
|
: Inula.useEffect
|
||||||
|
let cacheIntlConfig = null;
|
||||||
|
|
||||||
|
const getIntlConfig = () => {
|
||||||
|
if(!cacheIntlConfig){
|
||||||
|
cacheIntlConfig = getPluginManager().applyPlugins({
|
||||||
|
key: 'modifyIntlData',
|
||||||
|
type: '${api.ApplyPluginsType.modify}',
|
||||||
|
initialValue: localeInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cacheIntlConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RootContainer(props: any) {
|
||||||
|
|
||||||
|
const [locale,setLocale] = Inula.useState(getLocale());
|
||||||
|
const messages = getIntlConfig();
|
||||||
|
const handleLangChange = (locale:string) => {
|
||||||
|
setLocale(locale);
|
||||||
|
};
|
||||||
|
|
||||||
|
useIsomorphicLayoutEffect(() => {
|
||||||
|
event.on(LANG_CHANGE_EVENT, handleLangChange);
|
||||||
|
return () => {
|
||||||
|
event.off(LANG_CHANGE_EVENT, handleLangChange);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return <IntlProvider locale={locale} messages={messages[locale]}>{props.children}</IntlProvider>;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
// runtime.tsx
|
||||||
|
api.writeTmpFile({
|
||||||
|
path: 'runtime.tsx',
|
||||||
|
content: `
|
||||||
|
import React from 'react';
|
||||||
|
import { RootContainer } from './intl';
|
||||||
|
|
||||||
|
export function i18nProvider(container, opts) {
|
||||||
|
return React.createElement(RootContainer, opts, container);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
// index.ts for export
|
||||||
|
api.writeTmpFile({
|
||||||
|
path: 'index.ts',
|
||||||
|
content: `export { useIntl, FormattedMessage } from '${intlPath}';
|
||||||
|
export { addLocale, setLocale, getLocale, getAllLocales } from './localeExports';
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const localeExportsTpl = readFileSync(
|
||||||
|
join(TEMPLATES_DIR, 'localeExports.tpl'),
|
||||||
|
'utf-8',
|
||||||
|
);
|
||||||
|
const localeDirName = 'locales';
|
||||||
|
const localeDirPath = join(api.paths!.absSrcPath!, localeDirName);
|
||||||
|
const EventEmitterPkg = winPath(
|
||||||
|
dirname(require.resolve('event-emitter/package')),
|
||||||
|
);
|
||||||
|
const defaultLocale = api.config.intl?.default || `zh-CN`;
|
||||||
|
const localeList = await getList();
|
||||||
|
const reactIntlPkgPath = winPath(
|
||||||
|
dirname(require.resolve('inula-intl/package')),
|
||||||
|
);
|
||||||
|
api.writeTmpFile({
|
||||||
|
path: 'localeExports.ts',
|
||||||
|
content: Mustache.render(localeExportsTpl, {
|
||||||
|
EventEmitterPkg,
|
||||||
|
BaseSeparator: '-',
|
||||||
|
BaseNavigator: true,
|
||||||
|
UseLocalStorage: true,
|
||||||
|
LocaleDir: localeDirName,
|
||||||
|
ExistLocaleDir: existsSync(localeDirPath),
|
||||||
|
LocaleList: localeList.map((locale) => ({
|
||||||
|
...locale,
|
||||||
|
paths: locale.paths.map((path, index) => ({
|
||||||
|
path,
|
||||||
|
index,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
DefaultLocale: JSON.stringify(defaultLocale),
|
||||||
|
reactIntlPkgPath,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
api.addRuntimePlugin(() => {
|
||||||
|
return [withTmpPath({ api, path: 'runtime.tsx' })];
|
||||||
|
});
|
||||||
|
api.addTmpGenerateWatcherPaths(() => {
|
||||||
|
return [join(api.paths.absSrcPath, api.config?.intl.localeFolder)];
|
||||||
|
});
|
||||||
|
// 增加 api 供其他的插件使用
|
||||||
|
api.registerMethod({ name: 'modifyIntlData' });
|
||||||
|
let sendAi: IAzureSend;
|
||||||
|
api.onIntlAzure(async ({ send }) => {
|
||||||
|
sendAi = send;
|
||||||
|
});
|
||||||
|
api.registerGenerator({
|
||||||
|
key: 'intl',
|
||||||
|
name: 'Generate Intl',
|
||||||
|
description: '新建一个 Intl 文件',
|
||||||
|
type: GeneratorType.generate,
|
||||||
|
fn: async ({ args }) => {
|
||||||
|
const name = args?._?.[1];
|
||||||
|
let defaultCode = `export default {
|
||||||
|
'navBar.lang': '语言',
|
||||||
|
};`;
|
||||||
|
let defaultPath = name;
|
||||||
|
if (args?.create) {
|
||||||
|
logger.info('[试验性方案] 使用 aigc 自动翻译');
|
||||||
|
const localeList = await getList();
|
||||||
|
const defaultLocaleLang = api.config.intl?.default || `zh-CN`;
|
||||||
|
// 把现在所有的语言记录下来
|
||||||
|
const allLocales: string[] = [];
|
||||||
|
const defaultLocale = localeList
|
||||||
|
.map((local) => {
|
||||||
|
allLocales.push(local.name ?? '');
|
||||||
|
return local;
|
||||||
|
})
|
||||||
|
.filter((local) => local.name === defaultLocaleLang);
|
||||||
|
if (!defaultLocale || defaultLocale.length === 0) {
|
||||||
|
logger.error('未找到默认国际化语言文本,请检查配置');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
register.register({
|
||||||
|
implementor: esbuild,
|
||||||
|
exts: ['.ts', '.mjs'],
|
||||||
|
});
|
||||||
|
register.clearFiles();
|
||||||
|
let ret;
|
||||||
|
try {
|
||||||
|
ret = require(defaultLocale[0].paths[0]);
|
||||||
|
} catch (e: any) {
|
||||||
|
throw new Error(
|
||||||
|
`Register ${defaultLocale[0].paths[0]} failed, since ${e.message}`,
|
||||||
|
{ cause: e },
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
register.restore();
|
||||||
|
}
|
||||||
|
// 找到翻译的所有文本
|
||||||
|
const tfData = ret.__esModule ? ret.default : ret;
|
||||||
|
// 找到可以翻译的语言
|
||||||
|
const canUseLocale = Object.keys(DefaultLangUConfigMap).filter(
|
||||||
|
(i) => !allLocales.includes(i),
|
||||||
|
);
|
||||||
|
const { gType } = await prompts({
|
||||||
|
type: 'select',
|
||||||
|
name: 'gType',
|
||||||
|
message: 'Pick generator type',
|
||||||
|
choices: canUseLocale.map((key) => {
|
||||||
|
const item = DefaultLangUConfigMap[key];
|
||||||
|
return {
|
||||||
|
title: LangCnLabel[item.lang],
|
||||||
|
value: item.lang,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const msg = `请求 AIGC 对国际化文本进行翻译`;
|
||||||
|
logger.profile(msg);
|
||||||
|
const result = await sendAi(
|
||||||
|
`请将以下代码中的所有中文替换成${
|
||||||
|
LangCnLabel[gType]
|
||||||
|
},无需任何解释,请直接返回修改后的代码。代码如下:${JSON.stringify(
|
||||||
|
tfData,
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
logger.profile(msg);
|
||||||
|
const content = result.choices[0]!.message?.content || '{}';
|
||||||
|
if (content) {
|
||||||
|
const regex = /{([^}]*)}/;
|
||||||
|
// 加个保险,以防 AIGC 心情好加了文字说明
|
||||||
|
// @ts-ignore
|
||||||
|
const res = content?.match(regex)[0];
|
||||||
|
defaultCode = `export default ${res}`;
|
||||||
|
defaultPath = gType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const localesPath = join(
|
||||||
|
api.paths.absSrcPath,
|
||||||
|
api.config.intl.localeFolder,
|
||||||
|
);
|
||||||
|
fsExtra.outputFileSync(
|
||||||
|
join(localesPath, `${defaultPath}.ts`),
|
||||||
|
defaultCode,
|
||||||
|
);
|
||||||
|
logger.info('生成 intl 完成');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { glob, lodash, winPath } from '@umijs/utils';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { basename, join } from 'path';
|
||||||
|
|
||||||
|
export interface IGetLocaleFileListOpts {
|
||||||
|
localeFolder: string;
|
||||||
|
separator?: string;
|
||||||
|
absSrcPath?: string;
|
||||||
|
absPagesPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetLocaleFileListResult {
|
||||||
|
lang: string;
|
||||||
|
country: string;
|
||||||
|
name: string;
|
||||||
|
paths: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLocaleList = async (
|
||||||
|
opts: IGetLocaleFileListOpts,
|
||||||
|
): Promise<IGetLocaleFileListResult[]> => {
|
||||||
|
const {
|
||||||
|
localeFolder,
|
||||||
|
separator = '-',
|
||||||
|
absSrcPath = '',
|
||||||
|
absPagesPath = '',
|
||||||
|
} = opts;
|
||||||
|
const localeFileMath = new RegExp(
|
||||||
|
`^([a-z]{2})${separator}?([A-Z]{2})?\.(js|json|ts)$`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const localeFiles = glob
|
||||||
|
.sync('*.{ts,js,json}', {
|
||||||
|
cwd: winPath(join(absSrcPath, localeFolder)),
|
||||||
|
})
|
||||||
|
.map((name) => winPath(join(absSrcPath, localeFolder, name)))
|
||||||
|
.concat(
|
||||||
|
glob
|
||||||
|
.sync(`**/${localeFolder}/*.{ts,js,json}`, {
|
||||||
|
cwd: absPagesPath,
|
||||||
|
})
|
||||||
|
.map((name) => winPath(join(absPagesPath, name))),
|
||||||
|
)
|
||||||
|
.filter((p) => localeFileMath.test(basename(p)) && existsSync(p))
|
||||||
|
.map((fullName) => {
|
||||||
|
const fileName = basename(fullName);
|
||||||
|
const fileInfo = localeFileMath
|
||||||
|
.exec(fileName)
|
||||||
|
?.slice(1, 3)
|
||||||
|
?.filter(Boolean);
|
||||||
|
return {
|
||||||
|
name: (fileInfo || []).join(separator),
|
||||||
|
path: fullName,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const groups = lodash.groupBy(localeFiles, 'name');
|
||||||
|
|
||||||
|
const promises = Object.keys(groups).map(async (name) => {
|
||||||
|
const [lang, country = ''] = name.split(separator);
|
||||||
|
|
||||||
|
return {
|
||||||
|
lang,
|
||||||
|
name,
|
||||||
|
// react-intl Function.supportedLocalesOf
|
||||||
|
// Uncaught RangeError: Incorrect locale information provided
|
||||||
|
locale: name.split(separator).join('-'),
|
||||||
|
country,
|
||||||
|
paths: groups[name].map((item) => winPath(item.path)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return Promise.all(promises);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exactLocalePaths = (
|
||||||
|
data: IGetLocaleFileListResult[],
|
||||||
|
): string[] => {
|
||||||
|
return lodash.flatten(data.map((item) => item.paths));
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isNeedPolyfill(targets = {}) {
|
||||||
|
// data come from https://caniuse.com/#search=intl
|
||||||
|
// you can find all browsers in https://github.com/browserslist/browserslist#browsers
|
||||||
|
const polyfillTargets = {
|
||||||
|
ie: 10,
|
||||||
|
firefox: 28,
|
||||||
|
chrome: 23,
|
||||||
|
safari: 9.1,
|
||||||
|
opera: 12.1,
|
||||||
|
ios: 9.3,
|
||||||
|
ios_saf: 9.3,
|
||||||
|
operamini: Infinity,
|
||||||
|
op_mini: Infinity,
|
||||||
|
android: 4.3,
|
||||||
|
blackberry: Infinity,
|
||||||
|
operamobile: 12.1,
|
||||||
|
op_mob: 12.1,
|
||||||
|
explorermobil: 10,
|
||||||
|
ie_mob: 10,
|
||||||
|
ucandroid: Infinity,
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
Object.keys(targets).find((key) => {
|
||||||
|
const lowKey = key.toLocaleLowerCase();
|
||||||
|
// @ts-ignore
|
||||||
|
return polyfillTargets[lowKey] && polyfillTargets[lowKey] >= targets[key];
|
||||||
|
}) !== undefined
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { winPath } from '@umijs/utils';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export function withTmpPath(opts: {
|
||||||
|
api: IApi;
|
||||||
|
path: string;
|
||||||
|
noPluginDir?: boolean;
|
||||||
|
}) {
|
||||||
|
return winPath(
|
||||||
|
join(
|
||||||
|
opts.api.paths.absTmpPath,
|
||||||
|
opts.api.plugin.key && !opts.noPluginDir
|
||||||
|
? `plugin-${opts.api.plugin.key}`
|
||||||
|
: '',
|
||||||
|
opts.path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
import EventEmitter from '{{{EventEmitterPkg}}}';
|
||||||
|
const useLocalStorage = {{{UseLocalStorage}}};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
export const event = new EventEmitter();
|
||||||
|
|
||||||
|
export const LANG_CHANGE_EVENT = Symbol('LANG_CHANGE');
|
||||||
|
|
||||||
|
{{#LocaleList}}
|
||||||
|
{{#paths}}
|
||||||
|
import lang_{{lang}}{{country}}{{index}} from "{{{path}}}";
|
||||||
|
{{/paths}}
|
||||||
|
{{/LocaleList}}
|
||||||
|
|
||||||
|
const flattenMessages=(
|
||||||
|
nestedMessages: Record<string, any>,
|
||||||
|
prefix = '',
|
||||||
|
) => {
|
||||||
|
return Object.keys(nestedMessages).reduce(
|
||||||
|
(messages: Record<string, any>, key) => {
|
||||||
|
const value = nestedMessages[key];
|
||||||
|
const prefixedKey = prefix ? `${prefix}.${key}` : key;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
messages[prefixedKey] = value;
|
||||||
|
} else {
|
||||||
|
Object.assign(messages, flattenMessages(value, prefixedKey));
|
||||||
|
}
|
||||||
|
return messages;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const localeInfo: {[key: string]: any} = {
|
||||||
|
{{#LocaleList}}
|
||||||
|
'{{name}}': {
|
||||||
|
{{#paths}}...flattenMessages(lang_{{lang}}{{country}}{{index}}),{{/paths}}
|
||||||
|
},
|
||||||
|
{{/LocaleList}}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加一个新的国际化语言
|
||||||
|
* @param name 语言的 key
|
||||||
|
* @param messages 对应的枚举对象
|
||||||
|
*/
|
||||||
|
export const addLocale = (
|
||||||
|
name: string,
|
||||||
|
messages: Object,
|
||||||
|
) => {
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 可以合并
|
||||||
|
const mergeMessages = localeInfo[name]
|
||||||
|
? Object.assign({}, localeInfo[name], messages)
|
||||||
|
: messages;
|
||||||
|
|
||||||
|
localeInfo[name] = mergeMessages;
|
||||||
|
// 如果这是的 name 和当前的locale 相同需要重新设置一下,不然更新不了
|
||||||
|
if (name === getLocale()) {
|
||||||
|
event.emit(LANG_CHANGE_EVENT, name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前选择的语言
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
export const getLocale = () => {
|
||||||
|
// because changing will break the app
|
||||||
|
const lang =
|
||||||
|
navigator.cookieEnabled && typeof localStorage !== 'undefined' && useLocalStorage
|
||||||
|
? window.localStorage.getItem('inula_locale')
|
||||||
|
: '';
|
||||||
|
// support baseNavigator, default true
|
||||||
|
let browserLang;
|
||||||
|
{{#BaseNavigator}}
|
||||||
|
const isNavigatorLanguageValid =
|
||||||
|
typeof navigator !== 'undefined' && typeof navigator.language === 'string';
|
||||||
|
browserLang = isNavigatorLanguageValid
|
||||||
|
? navigator.language
|
||||||
|
: '';
|
||||||
|
{{/BaseNavigator}}
|
||||||
|
return lang || browserLang || {{{DefaultLocale}}};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换语言
|
||||||
|
* @param lang 语言的 key
|
||||||
|
* @param realReload 是否刷新页面,默认刷新
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
export const setLocale = (lang: string, realReload: boolean = true) => {
|
||||||
|
//const { pluginManager } = useAppContext();
|
||||||
|
//const runtimeLocale = pluginManagerapplyPlugins({
|
||||||
|
// key: 'locale',
|
||||||
|
// workaround: 不使用 ApplyPluginsType.modify 是为了避免循环依赖,与 fast-refresh 一起用时会有问题
|
||||||
|
// type: 'modify',
|
||||||
|
// initialValue: {},
|
||||||
|
//});
|
||||||
|
|
||||||
|
const updater = () => {
|
||||||
|
if (getLocale() !== lang) {
|
||||||
|
if (navigator.cookieEnabled && typeof window.localStorage !== 'undefined' && useLocalStorage) {
|
||||||
|
window.localStorage.setItem('inula_locale', lang || '');
|
||||||
|
}
|
||||||
|
if (realReload) {
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
event.emit(LANG_CHANGE_EVENT, lang);
|
||||||
|
// chrome 不支持这个事件。所以人肉触发一下
|
||||||
|
if (window.dispatchEvent) {
|
||||||
|
const event = new Event('languagechange');
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updater();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取语言列表
|
||||||
|
* @returns string[]
|
||||||
|
*/
|
||||||
|
export const getAllLocales = () => Object.keys(localeInfo);
|
|
@ -0,0 +1,195 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { generateService, getSchema } from '@umijs/openapi';
|
||||||
|
import { lodash, resolve, winPath } from '@umijs/utils';
|
||||||
|
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import rimraf from 'rimraf';
|
||||||
|
import serveStatic from 'serve-static';
|
||||||
|
|
||||||
|
export function resolveProjectDep(opts: {
|
||||||
|
pkg: any;
|
||||||
|
cwd: string;
|
||||||
|
dep: string;
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
opts.pkg.dependencies?.[opts.dep] ||
|
||||||
|
opts.pkg.devDependencies?.[opts.dep]
|
||||||
|
) {
|
||||||
|
return dirname(
|
||||||
|
resolve.sync(`${opts.dep}/package.json`, {
|
||||||
|
basedir: opts.cwd,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
let swaggerPath: string;
|
||||||
|
try {
|
||||||
|
swaggerPath =
|
||||||
|
resolveProjectDep({
|
||||||
|
pkg: api.pkg,
|
||||||
|
cwd: api.cwd,
|
||||||
|
dep: 'swagger-ui-dist',
|
||||||
|
}) || dirname(require.resolve('swagger-ui-dist/package.json'));
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
api.describe({
|
||||||
|
key: 'openAPI',
|
||||||
|
config: {
|
||||||
|
schema(joi) {
|
||||||
|
const itemSchema = joi.object({
|
||||||
|
requestLibPath: joi.string(),
|
||||||
|
schemaPath: joi.string(),
|
||||||
|
mock: joi.boolean(),
|
||||||
|
projectName: joi.string(),
|
||||||
|
apiPrefix: joi.alternatives(joi.string(), joi.function()),
|
||||||
|
namespace: joi.string(),
|
||||||
|
hook: joi.object({
|
||||||
|
customFunctionName: joi.function(),
|
||||||
|
customClassName: joi.function(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
return joi.alternatives(joi.array().items(itemSchema), itemSchema);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
enableBy: api.EnableBy.config,
|
||||||
|
});
|
||||||
|
const { absNodeModulesPath, absTmpPath } = api.paths;
|
||||||
|
const openAPIFilesPath = join(absNodeModulesPath!, 'umi_open_api');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (existsSync(openAPIFilesPath)) {
|
||||||
|
rimraf.sync(openAPIFilesPath);
|
||||||
|
}
|
||||||
|
mkdirSync(join(openAPIFilesPath));
|
||||||
|
} catch (error) {
|
||||||
|
// console.log(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 增加中间件
|
||||||
|
api.addBeforeMiddlewares(() => {
|
||||||
|
return [serveStatic(openAPIFilesPath)];
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onGenerateFiles(() => {
|
||||||
|
const openAPIConfig = api.config.openAPI;
|
||||||
|
const arrayConfig = lodash.flatten([openAPIConfig]);
|
||||||
|
const config = arrayConfig?.[0]?.projectName || 'openapi';
|
||||||
|
api.writeTmpFile({
|
||||||
|
path: join('plugin-openapi', 'openapi.tsx'),
|
||||||
|
noPluginDir: true,
|
||||||
|
content: `
|
||||||
|
// This file is generated by Inula automatically
|
||||||
|
// DO NOT CHANGE IT MANUALLY!
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { SwaggerUIBundle } from '${swaggerPath}';
|
||||||
|
import '${swaggerPath}/swagger-ui.css';
|
||||||
|
const App = () => {
|
||||||
|
const [value, setValue] = useState("${config || 'openapi'}" );
|
||||||
|
useEffect(() => {
|
||||||
|
SwaggerUIBundle({
|
||||||
|
url: \`/inula-plugins_$\{value}.json\`,
|
||||||
|
dom_id: '#swagger-ui',
|
||||||
|
});
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: 24,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
right: "16px",
|
||||||
|
top: "8px",
|
||||||
|
}}
|
||||||
|
onChange={(e) => setValue(e.target.value)}
|
||||||
|
>
|
||||||
|
${arrayConfig
|
||||||
|
.map((item) => {
|
||||||
|
return `<option value="${item.projectName || 'openapi'}">${
|
||||||
|
item.projectName || 'openapi'
|
||||||
|
}</option>`;
|
||||||
|
})
|
||||||
|
.join('\n')}
|
||||||
|
</select>
|
||||||
|
<div id="swagger-ui" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default App;
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (api.env === 'development') {
|
||||||
|
api.modifyRoutes((routes) => {
|
||||||
|
routes['inula/plugin/openapi'] = {
|
||||||
|
path: '/inula/plugin/openapi',
|
||||||
|
absPath: '/inula/plugin/openapi',
|
||||||
|
id: 'inula/plugin/openapi',
|
||||||
|
file: winPath(join(absTmpPath!, 'plugin-openapi', 'openapi.tsx')),
|
||||||
|
};
|
||||||
|
return routes;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const genOpenAPIFiles = async (openAPIConfig: any) => {
|
||||||
|
const openAPIJson = await getSchema(openAPIConfig.schemaPath);
|
||||||
|
writeFileSync(
|
||||||
|
join(
|
||||||
|
openAPIFilesPath,
|
||||||
|
`inula-plugins_${openAPIConfig.projectName || 'openapi'}.json`,
|
||||||
|
),
|
||||||
|
JSON.stringify(openAPIJson, null, 2),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
api.onDevCompileDone(async () => {
|
||||||
|
try {
|
||||||
|
const openAPIConfig = api.config.openAPI;
|
||||||
|
if (Array.isArray(openAPIConfig)) {
|
||||||
|
openAPIConfig.map((item) => genOpenAPIFiles(item));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
genOpenAPIFiles(openAPIConfig);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const genAllFiles = async (openAPIConfig: any) => {
|
||||||
|
const pageConfig = require(join(api.cwd, 'package.json'));
|
||||||
|
const mockFolder = openAPIConfig.mock ? join(api.cwd, 'mock') : undefined;
|
||||||
|
const serversFolder = join(api.cwd, 'src', 'services');
|
||||||
|
// 如果mock 文件不存在,创建一下
|
||||||
|
if (mockFolder && !existsSync(mockFolder)) {
|
||||||
|
mkdirSync(mockFolder);
|
||||||
|
}
|
||||||
|
// 如果mock 文件不存在,创建一下
|
||||||
|
if (serversFolder && !existsSync(serversFolder)) {
|
||||||
|
mkdirSync(serversFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
await generateService({
|
||||||
|
projectName: pageConfig.name.split('/').pop(),
|
||||||
|
...openAPIConfig,
|
||||||
|
serversPath: serversFolder,
|
||||||
|
mockFolder,
|
||||||
|
});
|
||||||
|
api.logger.info('[openAPI]: execution complete');
|
||||||
|
};
|
||||||
|
api.registerCommand({
|
||||||
|
name: 'openapi',
|
||||||
|
fn: async () => {
|
||||||
|
const openAPIConfig = api.config.openAPI;
|
||||||
|
if (Array.isArray(openAPIConfig)) {
|
||||||
|
openAPIConfig.map((item) => genAllFiles(item));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: 用户没有 src/services 会报错
|
||||||
|
genAllFiles(openAPIConfig);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,13 @@
|
||||||
|
import * as Babel from '@umijs/bundler-utils/compiled/babel/core';
|
||||||
|
import * as t from '@umijs/bundler-utils/compiled/babel/types';
|
||||||
|
|
||||||
|
export function getIdentifierDeclaration(node: t.Node, path: Babel.NodePath) {
|
||||||
|
if (t.isIdentifier(node) && path.scope.hasBinding(node.name)) {
|
||||||
|
let bindingNode = path.scope.getBinding(node.name)!.path.node;
|
||||||
|
if (t.isVariableDeclarator(bindingNode)) {
|
||||||
|
bindingNode = bindingNode.init!;
|
||||||
|
}
|
||||||
|
return bindingNode;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { winPath } from '@umijs/utils';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description
|
||||||
|
* - `'javascript'`: try to match the file with extname `.{ts(x)|js(x)}`
|
||||||
|
* - `'css'`: try to match the file with extname `.{less|sass|scss|stylus|css}`
|
||||||
|
*/
|
||||||
|
type FileType = 'javascript' | 'css';
|
||||||
|
|
||||||
|
interface IGetFileOpts {
|
||||||
|
base: string;
|
||||||
|
type: FileType;
|
||||||
|
fileNameWithoutExt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extsMap: Record<FileType, string[]> = {
|
||||||
|
javascript: ['.ts', '.tsx', '.js', '.jsx'],
|
||||||
|
css: ['.less', '.sass', '.scss', '.stylus', '.css'],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to match the exact extname of the file in a specific directory.
|
||||||
|
* @returns
|
||||||
|
* - matched: `{ path: string; filename: string }`
|
||||||
|
* - otherwise: `null`
|
||||||
|
*/
|
||||||
|
export default function getFile(opts: IGetFileOpts) {
|
||||||
|
const exts = extsMap[opts.type];
|
||||||
|
for (const ext of exts) {
|
||||||
|
const filename = `${opts.fileNameWithoutExt}${ext}`;
|
||||||
|
const path = winPath(join(opts.base, filename));
|
||||||
|
if (existsSync(path)) {
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
filename,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import * as Babel from '@umijs/bundler-utils/compiled/babel/core';
|
||||||
|
import * as parser from '@umijs/bundler-utils/compiled/babel/parser';
|
||||||
|
import traverse from '@umijs/bundler-utils/compiled/babel/traverse';
|
||||||
|
import * as t from '@umijs/bundler-utils/compiled/babel/types';
|
||||||
|
import { Loader, transformSync } from '@umijs/bundler-utils/compiled/esbuild';
|
||||||
|
import { glob, winPath } from '@umijs/utils';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { basename, extname, join } from 'path';
|
||||||
|
import { getIdentifierDeclaration } from './astUtils';
|
||||||
|
|
||||||
|
interface IOpts {
|
||||||
|
contentTest?: (content: string) => Boolean;
|
||||||
|
astTest?: (opts: { node: t.Node; content: string }) => Boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Model {
|
||||||
|
file: string;
|
||||||
|
namespace: string;
|
||||||
|
id: string;
|
||||||
|
exportName: string;
|
||||||
|
constructor(file: string, id: number) {
|
||||||
|
let namespace;
|
||||||
|
let exportName;
|
||||||
|
const [_file, meta] = file.split('#');
|
||||||
|
if (meta) {
|
||||||
|
const metaObj: Record<string, string> = JSON.parse(meta);
|
||||||
|
namespace = metaObj.namespace;
|
||||||
|
exportName = metaObj.exportName;
|
||||||
|
}
|
||||||
|
this.file = _file;
|
||||||
|
this.id = `model_${id}`;
|
||||||
|
this.namespace = namespace || basename(file, extname(file));
|
||||||
|
this.exportName = exportName || 'default';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ModelUtils {
|
||||||
|
api: IApi;
|
||||||
|
opts: IOpts = {};
|
||||||
|
count: number = 1;
|
||||||
|
constructor(api: IApi | null, opts: IOpts) {
|
||||||
|
this.api = api as IApi;
|
||||||
|
this.opts = opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllModels(opts: { extraModels: string[] }) {
|
||||||
|
// reset count
|
||||||
|
this.count = 1;
|
||||||
|
return [
|
||||||
|
...this.getModels({
|
||||||
|
base: join(this.api.paths.absSrcPath, 'models'),
|
||||||
|
pattern: '**/*.{ts,tsx,js,jsx}',
|
||||||
|
}),
|
||||||
|
...this.getModels({
|
||||||
|
base: join(this.api.paths.absPagesPath),
|
||||||
|
pattern: '**/models/**/*.{ts,tsx,js,jsx}',
|
||||||
|
}),
|
||||||
|
...this.getModels({
|
||||||
|
base: join(this.api.paths.absPagesPath),
|
||||||
|
pattern: '**/model.{ts,tsx,js,jsx}',
|
||||||
|
}),
|
||||||
|
...opts.extraModels,
|
||||||
|
].map((file: string) => {
|
||||||
|
return new Model(file, this.count++);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getModels(opts: { base: string; pattern?: string }) {
|
||||||
|
return glob
|
||||||
|
.sync(opts.pattern || '**/*.{ts,js}', {
|
||||||
|
cwd: opts.base,
|
||||||
|
absolute: true,
|
||||||
|
})
|
||||||
|
.map(winPath)
|
||||||
|
.filter((file) => {
|
||||||
|
if (/\.d.ts$/.test(file)) return false;
|
||||||
|
if (/\.(test|e2e|spec).([jt])sx?$/.test(file)) return false;
|
||||||
|
const content = readFileSync(file, 'utf-8');
|
||||||
|
return this.isModelValid({ content, file });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isModelValid(opts: { content: string; file: string }) {
|
||||||
|
const { file, content } = opts;
|
||||||
|
|
||||||
|
if (this.opts.contentTest && this.opts.contentTest(content)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform with esbuild first
|
||||||
|
// to reduce unexpected ast problem
|
||||||
|
const loader = extname(file).slice(1) as Loader;
|
||||||
|
const result = transformSync(content, {
|
||||||
|
loader,
|
||||||
|
sourcemap: false,
|
||||||
|
minify: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// transform with babel
|
||||||
|
let ret = false;
|
||||||
|
const ast = parser.parse(result.code, {
|
||||||
|
sourceType: 'module',
|
||||||
|
sourceFilename: file,
|
||||||
|
plugins: [],
|
||||||
|
});
|
||||||
|
traverse(ast, {
|
||||||
|
ExportDefaultDeclaration: (
|
||||||
|
path: Babel.NodePath<t.ExportDefaultDeclaration>,
|
||||||
|
) => {
|
||||||
|
let node: any = path.node.declaration;
|
||||||
|
node = getIdentifierDeclaration(node, path);
|
||||||
|
if (this.opts.astTest && this.opts.astTest({ node, content })) {
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getModelsContent(models: Model[]) {
|
||||||
|
const imports: string[] = [];
|
||||||
|
const modelProps: string[] = [];
|
||||||
|
models.forEach((model) => {
|
||||||
|
if (model.exportName !== 'default') {
|
||||||
|
imports.push(
|
||||||
|
`import { ${model.exportName} as ${model.id} } from '${model.file}';`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
imports.push(`import ${model.id} from '${model.file}';`);
|
||||||
|
}
|
||||||
|
modelProps.push(
|
||||||
|
`${model.id}: { namespace: '${model.namespace}', model: ${model.id} },`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return `
|
||||||
|
${imports.join('\n')}
|
||||||
|
|
||||||
|
export const models = {
|
||||||
|
${modelProps.join('\n')}
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
export default function resetMainPath(routes: any[], mainPath: string) {
|
||||||
|
let newPath = mainPath;
|
||||||
|
// 把用户输入/abc/ 转成 /abc
|
||||||
|
if (newPath !== '/' && newPath.slice(-1) === '/') {
|
||||||
|
newPath = newPath.slice(0, -1);
|
||||||
|
}
|
||||||
|
// 把用户输入abc 转成 /abc
|
||||||
|
if (newPath !== '/' && newPath.slice(0, 1) !== '/') {
|
||||||
|
newPath = `/${newPath}`;
|
||||||
|
}
|
||||||
|
return routes.map((element) => {
|
||||||
|
if (element.isResetMainEdit) {
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
if (element.path === '/' && !element.routes) {
|
||||||
|
element.path = '/index';
|
||||||
|
element.isResetMainEdit = true;
|
||||||
|
}
|
||||||
|
if (element.path === newPath) {
|
||||||
|
element.path = '/';
|
||||||
|
element.isResetMainEdit = true;
|
||||||
|
}
|
||||||
|
if (Array.isArray(element.routes)) {
|
||||||
|
element.routes = resetMainPath(element.routes, mainPath);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { resolve } from '@umijs/utils';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
export function resolveProjectDep(opts: {
|
||||||
|
pkg: any;
|
||||||
|
cwd: string;
|
||||||
|
dep: string;
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
opts.pkg.dependencies?.[opts.dep] ||
|
||||||
|
opts.pkg.devDependencies?.[opts.dep]
|
||||||
|
) {
|
||||||
|
return dirname(
|
||||||
|
resolve.sync(`${opts.dep}/package.json`, {
|
||||||
|
basedir: opts.cwd,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
const isWin = process.platform === 'win32';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* resolve src from dest
|
||||||
|
* @refer https://github.com/zkochan/symlink-dir/blob/master/src/index.ts#L18
|
||||||
|
*/
|
||||||
|
function resolveSrc(src: string, dest: string) {
|
||||||
|
return isWin ? `${src}\\` : path.relative(path.dirname(dest), src);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (src: string, dest: string) => {
|
||||||
|
const destDir = path.dirname(dest);
|
||||||
|
const resolvedSrc = resolveSrc(src, dest);
|
||||||
|
// see also: https://github.com/zkochan/symlink-dir/blob/master/src/index.ts#L14
|
||||||
|
const symlinkType = isWin ? 'junction' : 'dir';
|
||||||
|
|
||||||
|
// create directory first if node_modules/@group not exists
|
||||||
|
if (!fs.existsSync(destDir)) {
|
||||||
|
fs.mkdirSync(destDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// create src symlink relative dest
|
||||||
|
fs.symlinkSync(resolvedSrc, dest, symlinkType);
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { winPath } from '@umijs/utils';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export function withTmpPath(opts: {
|
||||||
|
api: IApi;
|
||||||
|
path: string;
|
||||||
|
noPluginDir?: boolean;
|
||||||
|
}) {
|
||||||
|
return winPath(
|
||||||
|
join(
|
||||||
|
opts.api.paths.absTmpPath,
|
||||||
|
opts.api.plugin.key && !opts.noPluginDir
|
||||||
|
? `plugin-${opts.api.plugin.key}`
|
||||||
|
: '',
|
||||||
|
opts.path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export const TEMPLATES_DIR = join(__dirname, '../templates');
|
|
@ -0,0 +1,97 @@
|
||||||
|
import type { IApi } from '@aluni/types';
|
||||||
|
import { resolve, winPath } from '@umijs/utils';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { withTmpPath } from './withTmpPath';
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
api.describe({
|
||||||
|
key: 'request',
|
||||||
|
config: {
|
||||||
|
schema(zod) {
|
||||||
|
return zod.object();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
api.addRuntimePluginKey(() => ['request']);
|
||||||
|
|
||||||
|
// only dev or build running
|
||||||
|
if (!['dev', 'build', 'dev-config', 'preview', 'setup'].includes(api.name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
api.onGenerateFiles(() => {
|
||||||
|
// runtime.tsx
|
||||||
|
api.writeTmpFile({
|
||||||
|
path: 'runtime.tsx',
|
||||||
|
content: `
|
||||||
|
import { getPluginManager } from '../core/plugin';
|
||||||
|
import ir from '${winPath(dirname(require.resolve('inula-request/package')))}'
|
||||||
|
|
||||||
|
export function rootContainer(container) {
|
||||||
|
const irconfig = getPluginManager().applyPlugins({ key: 'request',type: 'modify', initialValue: {} });
|
||||||
|
Object.keys(irconfig).forEach(key=>{
|
||||||
|
// TODO: inula-request 的怪异传参方式
|
||||||
|
ir.defaults[key] = irconfig[key];
|
||||||
|
})
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
// index.ts for export
|
||||||
|
api.writeTmpFile({
|
||||||
|
path: 'index.ts',
|
||||||
|
content: `
|
||||||
|
export { default as ir, useIR } from '${winPath(
|
||||||
|
dirname(require.resolve('inula-request/package')),
|
||||||
|
)}';
|
||||||
|
export { useRequest } from '${winPath(
|
||||||
|
dirname(require.resolve('ahooks/package')),
|
||||||
|
)}';
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// types.ts
|
||||||
|
api.writeTmpFile({
|
||||||
|
path: 'types.d.ts',
|
||||||
|
tpl: `export { IrRequestConfig } from '${winPath(
|
||||||
|
dirname(require.resolve('inula-request/package')),
|
||||||
|
)}';`,
|
||||||
|
context: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
api.addRuntimePlugin(() => {
|
||||||
|
return [withTmpPath({ api, path: 'runtime.tsx' })];
|
||||||
|
});
|
||||||
|
|
||||||
|
api.chainWebpack((memo) => {
|
||||||
|
function getUserLibDir({ library }: { library: string }) {
|
||||||
|
if (
|
||||||
|
// @ts-ignore
|
||||||
|
(api.pkg.dependencies && api.pkg.dependencies[library]) ||
|
||||||
|
// @ts-ignore
|
||||||
|
(api.pkg.devDependencies && api.pkg.devDependencies[library]) ||
|
||||||
|
// egg project using `clientDependencies` in ali tnpm
|
||||||
|
// @ts-ignore
|
||||||
|
(api.pkg.clientDependencies && api.pkg.clientDependencies[library])
|
||||||
|
) {
|
||||||
|
return winPath(
|
||||||
|
dirname(
|
||||||
|
// 通过 resolve 往上找,可支持 lerna 仓库
|
||||||
|
// lerna 仓库如果用 yarn workspace 的依赖不一定在 node_modules,可能被提到根目录,并且没有 link
|
||||||
|
resolve.sync(`${library}/package.json`, {
|
||||||
|
basedir: api.paths.cwd,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 用户也可以通过显示安装 antd-mobile-v2,升级版本
|
||||||
|
memo.resolve.alias.set(
|
||||||
|
'ahooks',
|
||||||
|
getUserLibDir({ library: 'ahooks' }) ||
|
||||||
|
dirname(require.resolve('ahooks/package.json')),
|
||||||
|
);
|
||||||
|
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { winPath } from '@umijs/utils';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export function withTmpPath(opts: {
|
||||||
|
api: IApi;
|
||||||
|
path: string;
|
||||||
|
noPluginDir?: boolean;
|
||||||
|
}) {
|
||||||
|
return winPath(
|
||||||
|
join(
|
||||||
|
opts.api.paths.absTmpPath,
|
||||||
|
opts.api.plugin.key && !opts.noPluginDir
|
||||||
|
? `plugin-${opts.api.plugin.key}`
|
||||||
|
: '',
|
||||||
|
opts.path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import * as Babel from '@umijs/bundler-utils/compiled/babel/core';
|
||||||
|
import * as t from '@umijs/bundler-utils/compiled/babel/types';
|
||||||
|
|
||||||
|
export function getIdentifierDeclaration(node: t.Node, path: Babel.NodePath) {
|
||||||
|
if (t.isIdentifier(node) && path.scope.hasBinding(node.name)) {
|
||||||
|
let bindingNode = path.scope.getBinding(node.name)!.path.node;
|
||||||
|
if (t.isVariableDeclarator(bindingNode)) {
|
||||||
|
bindingNode = bindingNode.init!;
|
||||||
|
}
|
||||||
|
return bindingNode;
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export const INULA_KEYS = ['create', 'use', 'clear'];
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { fsExtra, logger } from '@umijs/utils';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { StoreUtils } from './storesUtils';
|
||||||
|
|
||||||
|
export enum GeneratorType {
|
||||||
|
generate = 'generate',
|
||||||
|
enable = 'enable',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
api.describe({
|
||||||
|
key: 'stores',
|
||||||
|
config: {
|
||||||
|
schema({ zod }) {
|
||||||
|
return zod.boolean();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.modifyAppData((memo) => {
|
||||||
|
const stores = getAllStores(api);
|
||||||
|
memo.pluginX = {
|
||||||
|
stores,
|
||||||
|
};
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onGenerateFiles((args) => {
|
||||||
|
const stores = args.isFirstTime
|
||||||
|
? api.appData.pluginX.stores
|
||||||
|
: getAllStores(api);
|
||||||
|
// index.ts for export
|
||||||
|
api.writeTmpFile({
|
||||||
|
path: 'index.ts',
|
||||||
|
content: StoreUtils.getStoresContent(stores),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
api.addTmpGenerateWatcherPaths(() => {
|
||||||
|
return [join(api.paths.absSrcPath, 'stores')];
|
||||||
|
});
|
||||||
|
|
||||||
|
api.registerGenerator({
|
||||||
|
key: 'x',
|
||||||
|
name: 'Enable Store',
|
||||||
|
description: '新建一个 Store',
|
||||||
|
type: GeneratorType.generate,
|
||||||
|
fn: async ({ args }) => {
|
||||||
|
const name = args?._?.[1];
|
||||||
|
const storesPath = join(api.paths.absSrcPath, 'stores');
|
||||||
|
fsExtra.outputFileSync(
|
||||||
|
join(storesPath, `${name}.ts`),
|
||||||
|
`import { createStore } from '${api.appData.umi.importSource}';
|
||||||
|
|
||||||
|
export default createStore({
|
||||||
|
id: '${name}',
|
||||||
|
actions: {
|
||||||
|
changeName: (state,value) => {
|
||||||
|
state.title = value || 'openinula';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
title: 'inula',
|
||||||
|
},
|
||||||
|
});`,
|
||||||
|
);
|
||||||
|
logger.info('生成 store 完成');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function getStoreUtil(api: IApi | null) {
|
||||||
|
return new StoreUtils(api);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllStores(api: IApi) {
|
||||||
|
return getStoreUtil(api).getAllStores({
|
||||||
|
extraStores: [],
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,275 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { prettyPrintEsBuildErrors } from '@umijs/bundler-utils';
|
||||||
|
import * as Babel from '@umijs/bundler-utils/compiled/babel/core';
|
||||||
|
import * as parser from '@umijs/bundler-utils/compiled/babel/parser';
|
||||||
|
import traverse from '@umijs/bundler-utils/compiled/babel/traverse';
|
||||||
|
import * as t from '@umijs/bundler-utils/compiled/babel/types';
|
||||||
|
import {
|
||||||
|
Loader,
|
||||||
|
transformSync,
|
||||||
|
type TransformResult,
|
||||||
|
} from '@umijs/bundler-utils/compiled/esbuild';
|
||||||
|
import { glob, winPath } from '@umijs/utils';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { basename, dirname, extname, format, join, relative } from 'path';
|
||||||
|
import { getIdentifierDeclaration } from './astUtils';
|
||||||
|
import { INULA_KEYS } from './constants';
|
||||||
|
|
||||||
|
interface IOpts {
|
||||||
|
contentTest?: (content: string) => Boolean;
|
||||||
|
astTest?: (opts: { node: t.Node; content: string }) => Boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNamespace(absFilePath: string, absSrcPath: string) {
|
||||||
|
const relPath = winPath(relative(winPath(absSrcPath), winPath(absFilePath)));
|
||||||
|
const parts = relPath.split('/');
|
||||||
|
const dirs = parts.slice(0, -1);
|
||||||
|
const file = parts[parts.length - 1];
|
||||||
|
// src/pages/foo/stores/bar > foo/bar
|
||||||
|
const validDirs = dirs.filter(
|
||||||
|
(dir) => !['src', 'pages', 'stores'].includes(dir),
|
||||||
|
);
|
||||||
|
let normalizedFile = file;
|
||||||
|
normalizedFile = basename(file, extname(file));
|
||||||
|
// foo.store > foo
|
||||||
|
if (normalizedFile.endsWith('.store')) {
|
||||||
|
normalizedFile = normalizedFile.split('.').slice(0, -1).join('.');
|
||||||
|
}
|
||||||
|
return [...validDirs, normalizedFile].join('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Store {
|
||||||
|
file: string;
|
||||||
|
namespace: string;
|
||||||
|
id: string;
|
||||||
|
exportName: string;
|
||||||
|
deps: string[];
|
||||||
|
constructor(
|
||||||
|
file: string,
|
||||||
|
absSrcPath: string,
|
||||||
|
sort: {} | undefined,
|
||||||
|
id: number,
|
||||||
|
namesCache: any,
|
||||||
|
) {
|
||||||
|
let namespace;
|
||||||
|
let exportName;
|
||||||
|
const [_file, meta] = file.split('#');
|
||||||
|
if (meta) {
|
||||||
|
const metaObj: Record<string, string> = JSON.parse(meta);
|
||||||
|
namespace = metaObj.namespace;
|
||||||
|
exportName = metaObj.exportName;
|
||||||
|
}
|
||||||
|
this.file = _file;
|
||||||
|
this.id = `store_${id}`;
|
||||||
|
this.namespace =
|
||||||
|
namespace || namesCache[file] || getNamespace(_file, absSrcPath);
|
||||||
|
if (INULA_KEYS.includes(this.namespace)) {
|
||||||
|
const error = new Error(
|
||||||
|
`Store 导出命名为 ${this.namespace},${this.namespace}Store 为 openinula 保留关键字`,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
this.exportName = exportName || 'default';
|
||||||
|
this.deps = sort ? this.findDeps(sort) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
findDeps(sort: object) {
|
||||||
|
const content = readFileSync(this.file, 'utf-8');
|
||||||
|
|
||||||
|
// transform with esbuild first
|
||||||
|
// to reduce unexpected ast problem
|
||||||
|
const loader = extname(this.file).slice(1) as Loader;
|
||||||
|
const result = transformSync(content, {
|
||||||
|
loader,
|
||||||
|
sourcemap: false,
|
||||||
|
minify: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// transform with babel
|
||||||
|
const deps = new Set<string>();
|
||||||
|
const ast = parser.parse(result.code, {
|
||||||
|
sourceType: 'module',
|
||||||
|
sourceFilename: this.file,
|
||||||
|
plugins: [],
|
||||||
|
});
|
||||||
|
// TODO: use sort
|
||||||
|
sort;
|
||||||
|
traverse(ast, {
|
||||||
|
CallExpression: (path: Babel.NodePath<t.CallExpression>) => {
|
||||||
|
if (
|
||||||
|
t.isIdentifier(path.node.callee, { name: 'useStore' }) &&
|
||||||
|
t.isStringLiteral(path.node.arguments[0])
|
||||||
|
) {
|
||||||
|
deps.add(path.node.arguments[0].value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return [...deps];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StoreUtils {
|
||||||
|
api: IApi;
|
||||||
|
opts: IOpts = {};
|
||||||
|
count: number = 1;
|
||||||
|
namespaceCache: any = {};
|
||||||
|
constructor(api: IApi | null, opts?: IOpts) {
|
||||||
|
this.api = api as IApi;
|
||||||
|
this.opts = opts || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllStores(opts: { sort?: object; extraStores: string[] }) {
|
||||||
|
// reset count
|
||||||
|
this.count = 1;
|
||||||
|
const stores = [
|
||||||
|
...this.getStores({
|
||||||
|
base: join(this.api.paths.absSrcPath, 'stores'),
|
||||||
|
pattern: '**/*.{ts,tsx,js,jsx}',
|
||||||
|
}),
|
||||||
|
...this.getStores({
|
||||||
|
base: join(this.api.paths.absPagesPath),
|
||||||
|
pattern: '**/stores/**/*.{ts,tsx,js,jsx}',
|
||||||
|
}),
|
||||||
|
...this.getStores({
|
||||||
|
base: join(this.api.paths.absPagesPath),
|
||||||
|
pattern: '**/store.{ts,tsx,js,jsx}',
|
||||||
|
}),
|
||||||
|
...opts.extraStores,
|
||||||
|
].map((file: string) => {
|
||||||
|
return new Store(
|
||||||
|
file,
|
||||||
|
this.api.paths.absSrcPath,
|
||||||
|
opts.sort,
|
||||||
|
this.count++,
|
||||||
|
this.namespaceCache,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return stores;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStores(opts: { base: string; pattern?: string }) {
|
||||||
|
return glob
|
||||||
|
.sync(opts.pattern || '**/*.{ts,js}', {
|
||||||
|
cwd: opts.base,
|
||||||
|
absolute: true,
|
||||||
|
})
|
||||||
|
.map(winPath)
|
||||||
|
.filter((file) => {
|
||||||
|
if (/\.d.ts$/.test(file)) return false;
|
||||||
|
if (/\.(test|e2e|spec).([jt])sx?$/.test(file)) return false;
|
||||||
|
const content = readFileSync(file, 'utf-8');
|
||||||
|
return this.isStoreValid({ content, file });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isStoreValid(opts: { content: string; file: string }) {
|
||||||
|
const { file, content } = opts;
|
||||||
|
|
||||||
|
// if (this.opts.contentTest && this.opts.contentTest(content)) {
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
let result: TransformResult | null = null;
|
||||||
|
try {
|
||||||
|
// transform with esbuild first
|
||||||
|
// to reduce unexpected ast problem
|
||||||
|
const ext = extname(file).slice(1);
|
||||||
|
const loader = ext === 'js' ? 'jsx' : (ext as Loader);
|
||||||
|
result = transformSync(content, {
|
||||||
|
loader,
|
||||||
|
sourcemap: false,
|
||||||
|
minify: false,
|
||||||
|
sourcefile: file,
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.errors?.length) {
|
||||||
|
prettyPrintEsBuildErrors(e.errors, { path: file, content });
|
||||||
|
delete e.errors;
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// transform with babel
|
||||||
|
let ret = false;
|
||||||
|
const ast = parser.parse(result!.code, {
|
||||||
|
sourceType: 'module',
|
||||||
|
sourceFilename: file,
|
||||||
|
plugins: [],
|
||||||
|
});
|
||||||
|
traverse(ast, {
|
||||||
|
ExportDefaultDeclaration: (
|
||||||
|
path: Babel.NodePath<t.ExportDefaultDeclaration>,
|
||||||
|
) => {
|
||||||
|
let node: any = path.node.declaration;
|
||||||
|
node = getIdentifierDeclaration(node, path);
|
||||||
|
// TODO: 强制写法 export default createStore(),后续调整是否需要修改
|
||||||
|
ret =
|
||||||
|
t.isCallExpression(node) &&
|
||||||
|
t.isIdentifier(node.callee) &&
|
||||||
|
node.callee.name === 'createStore';
|
||||||
|
},
|
||||||
|
ObjectExpression: (path: Babel.NodePath<t.ObjectExpression>) => {
|
||||||
|
let node: any = path.node;
|
||||||
|
if (t.isObjectExpression(node)) {
|
||||||
|
node.properties.some((property) => {
|
||||||
|
if ((property as any).key.name === 'id') {
|
||||||
|
// 将 id 取出来当 namespace
|
||||||
|
this.namespaceCache[file] = (property as any).value.value;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'state',
|
||||||
|
'reducers',
|
||||||
|
'subscriptions',
|
||||||
|
'effects',
|
||||||
|
'namespace',
|
||||||
|
].includes((property as any).key.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
static getStoresContent(stores: Store[]) {
|
||||||
|
const imports: string[] = [];
|
||||||
|
const namespace: any = {};
|
||||||
|
stores.forEach((store) => {
|
||||||
|
const fileWithoutExt = winPath(
|
||||||
|
format({
|
||||||
|
dir: dirname(store.file),
|
||||||
|
base: basename(store.file, extname(store.file)),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (store.exportName !== 'default') {
|
||||||
|
if (namespace[store.exportName]) {
|
||||||
|
const error = new Error(
|
||||||
|
`Store 导出命名重复:${
|
||||||
|
namespace[store.exportName]
|
||||||
|
} 和 ${fileWithoutExt}`,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
namespace[store.exportName] = fileWithoutExt;
|
||||||
|
imports.push(
|
||||||
|
`export { ${store.exportName} } from '${fileWithoutExt}';`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (namespace[store.namespace]) {
|
||||||
|
const error = new Error(
|
||||||
|
`Store 导出命名重复:${
|
||||||
|
namespace[store.namespace]
|
||||||
|
} 和 ${fileWithoutExt}`,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
namespace[store.namespace] = fileWithoutExt;
|
||||||
|
imports.push(
|
||||||
|
`export { default as ${store.namespace}Store } from '${fileWithoutExt}';`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return `
|
||||||
|
${imports.join('\n')}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { winPath } from '@umijs/utils';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export function withTmpPath(opts: {
|
||||||
|
api: IApi;
|
||||||
|
path: string;
|
||||||
|
noPluginDir?: boolean;
|
||||||
|
}) {
|
||||||
|
return winPath(
|
||||||
|
join(
|
||||||
|
opts.api.paths.absTmpPath,
|
||||||
|
opts.api.plugin.key && !opts.noPluginDir
|
||||||
|
? `plugin-${opts.api.plugin.key}`
|
||||||
|
: '',
|
||||||
|
opts.path,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,217 @@
|
||||||
|
import { getAssetsMap } from '@umijs/preset-umi/dist/commands/dev/getAssetsMap';
|
||||||
|
import { getBabelOpts } from '@umijs/preset-umi/dist/commands/dev/getBabelOpts';
|
||||||
|
import { getMarkupArgs } from '@umijs/preset-umi/dist/commands/dev/getMarkupArgs';
|
||||||
|
import { printMemoryUsage } from '@umijs/preset-umi/dist/commands/dev/printMemoryUsage';
|
||||||
|
import type { IApi, IOnGenerateFiles } from '@umijs/preset-umi/dist/types';
|
||||||
|
import {
|
||||||
|
measureFileSizesBeforeBuild,
|
||||||
|
printFileSizesAfterBuild,
|
||||||
|
} from '@umijs/preset-umi/dist/utils/fileSizeReporter';
|
||||||
|
import { lazyImportFromCurrentPkg } from '@umijs/preset-umi/dist/utils/lazyImportFromCurrentPkg';
|
||||||
|
import { getMarkup } from '@umijs/server';
|
||||||
|
import { chalk, fsExtra, logger, rimraf } from '@umijs/utils';
|
||||||
|
import { writeFileSync } from 'fs';
|
||||||
|
import { dirname, join, resolve } from 'path';
|
||||||
|
|
||||||
|
const bundlerWebpack: typeof import('@umijs/bundler-webpack') =
|
||||||
|
lazyImportFromCurrentPkg('@umijs/bundler-webpack');
|
||||||
|
const bundlerVite: typeof import('@umijs/bundler-vite') =
|
||||||
|
lazyImportFromCurrentPkg('@umijs/bundler-vite');
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
api.registerCommand({
|
||||||
|
name: 'build',
|
||||||
|
description: 'build app for production',
|
||||||
|
details: `
|
||||||
|
inula build
|
||||||
|
|
||||||
|
# build without compression
|
||||||
|
COMPRESS=none inula build
|
||||||
|
|
||||||
|
# clean and build
|
||||||
|
inula build --clean
|
||||||
|
`,
|
||||||
|
fn: async function () {
|
||||||
|
logger.info(chalk.cyan.bold(`Inula v${api.appData.umi.version}`));
|
||||||
|
|
||||||
|
// clear tmp
|
||||||
|
rimraf.sync(api.paths.absTmpPath);
|
||||||
|
|
||||||
|
// check package.json
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'onCheckPkgJSON',
|
||||||
|
args: {
|
||||||
|
origin: null,
|
||||||
|
current: api.appData.pkg,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// generate files
|
||||||
|
async function generate(opts: IOnGenerateFiles) {
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'onGenerateFiles',
|
||||||
|
args: {
|
||||||
|
files: opts.files || null,
|
||||||
|
isFirstTime: opts.isFirstTime,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await generate({
|
||||||
|
isFirstTime: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// build
|
||||||
|
// TODO: support watch mode
|
||||||
|
const {
|
||||||
|
babelPreset,
|
||||||
|
beforeBabelPlugins,
|
||||||
|
beforeBabelPresets,
|
||||||
|
extraBabelPlugins,
|
||||||
|
extraBabelPresets,
|
||||||
|
} = await getBabelOpts({ api });
|
||||||
|
const chainWebpack = async (memo: any, args: Object) => {
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'chainWebpack',
|
||||||
|
type: api.ApplyPluginsType.modify,
|
||||||
|
initialValue: memo,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const modifyWebpackConfig = async (memo: any, args: Object) => {
|
||||||
|
return await api.applyPlugins({
|
||||||
|
key: 'modifyWebpackConfig',
|
||||||
|
initialValue: memo,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const modifyViteConfig = async (memo: any, args: Object) => {
|
||||||
|
return await api.applyPlugins({
|
||||||
|
key: 'modifyViteConfig',
|
||||||
|
initialValue: memo,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const entry = await api.applyPlugins({
|
||||||
|
key: 'modifyEntry',
|
||||||
|
initialValue: {
|
||||||
|
umi: join(api.paths.absTmpPath, 'umi.ts'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const opts = {
|
||||||
|
config: api.config,
|
||||||
|
cwd: api.cwd,
|
||||||
|
entry,
|
||||||
|
...(api.config.vite
|
||||||
|
? { modifyViteConfig }
|
||||||
|
: { babelPreset, chainWebpack, modifyWebpackConfig }),
|
||||||
|
beforeBabelPlugins,
|
||||||
|
beforeBabelPresets,
|
||||||
|
extraBabelPlugins,
|
||||||
|
extraBabelPresets,
|
||||||
|
async onBuildComplete(opts: any) {
|
||||||
|
printMemoryUsage();
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'onBuildComplete',
|
||||||
|
args: opts,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clean: true,
|
||||||
|
htmlFiles: [] as any[],
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'onBeforeCompiler',
|
||||||
|
args: { compiler: api.config.vite ? 'vite' : 'webpack', opts },
|
||||||
|
});
|
||||||
|
|
||||||
|
let stats: any;
|
||||||
|
if (api.config.vite) {
|
||||||
|
stats = await bundlerVite.build(opts);
|
||||||
|
} else if (process.env.OKAM) {
|
||||||
|
require('@umijs/bundler-webpack/dist/requireHook');
|
||||||
|
const { build } = require(process.env.OKAM);
|
||||||
|
stats = await build(opts);
|
||||||
|
} else {
|
||||||
|
// Measure files sizes before build
|
||||||
|
const absOutputPath = resolve(
|
||||||
|
opts.cwd,
|
||||||
|
opts.config.outputPath || bundlerWebpack.DEFAULT_OUTPUT_PATH,
|
||||||
|
);
|
||||||
|
const previousFileSizes = measureFileSizesBeforeBuild(absOutputPath);
|
||||||
|
|
||||||
|
// Build
|
||||||
|
stats = await bundlerWebpack.build(opts);
|
||||||
|
|
||||||
|
// Print files sizes
|
||||||
|
console.log();
|
||||||
|
logger.info('File sizes after gzip:\n');
|
||||||
|
printFileSizesAfterBuild({
|
||||||
|
webpackStats: stats,
|
||||||
|
previousSizeMap: previousFileSizes,
|
||||||
|
buildFolder: absOutputPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate html
|
||||||
|
// vite 在 build 时通过插件注入 js 和 css
|
||||||
|
|
||||||
|
let htmlFiles: { path: string; content: string }[] = [];
|
||||||
|
|
||||||
|
if (!api.config.mpa) {
|
||||||
|
const assetsMap = api.config.vite
|
||||||
|
? {}
|
||||||
|
: getAssetsMap({
|
||||||
|
stats,
|
||||||
|
publicPath: api.config.publicPath,
|
||||||
|
});
|
||||||
|
const { vite } = api.args;
|
||||||
|
const markupArgs = await getMarkupArgs({ api });
|
||||||
|
const finalMarkUpArgs = {
|
||||||
|
...markupArgs,
|
||||||
|
styles: markupArgs.styles.concat(
|
||||||
|
api.config.vite
|
||||||
|
? []
|
||||||
|
: [...(assetsMap['umi.css'] || []).map((src) => ({ src }))],
|
||||||
|
),
|
||||||
|
scripts: (api.config.vite
|
||||||
|
? []
|
||||||
|
: [...(assetsMap['umi.js'] || []).map((src) => ({ src }))]
|
||||||
|
).concat(markupArgs.scripts),
|
||||||
|
esmScript: !!opts.config.esm || vite,
|
||||||
|
path: '/',
|
||||||
|
};
|
||||||
|
|
||||||
|
// allow to modify export html files
|
||||||
|
htmlFiles = await api.applyPlugins({
|
||||||
|
key: 'modifyExportHTMLFiles',
|
||||||
|
initialValue: [
|
||||||
|
{
|
||||||
|
path: 'index.html',
|
||||||
|
content: await getMarkup(finalMarkUpArgs),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
args: { markupArgs: finalMarkUpArgs, getMarkup },
|
||||||
|
});
|
||||||
|
|
||||||
|
htmlFiles.forEach(({ path, content }) => {
|
||||||
|
const absPath = resolve(api.paths.absOutputPath, path);
|
||||||
|
|
||||||
|
fsExtra.mkdirpSync(dirname(absPath));
|
||||||
|
writeFileSync(absPath, content, 'utf-8');
|
||||||
|
logger.event(`Build ${path}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// event when html is completed
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'onBuildHtmlComplete',
|
||||||
|
args: {
|
||||||
|
...opts,
|
||||||
|
htmlFiles,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// print size
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,425 @@
|
||||||
|
import type { RequestHandler } from '@umijs/bundler-webpack';
|
||||||
|
import { createRouteMiddleware } from '@umijs/preset-umi/dist/commands/dev/createRouteMiddleware';
|
||||||
|
import { faviconMiddleware } from '@umijs/preset-umi/dist/commands/dev/faviconMiddleware';
|
||||||
|
import { getBabelOpts } from '@umijs/preset-umi/dist/commands/dev/getBabelOpts';
|
||||||
|
import ViteHtmlPlugin from '@umijs/preset-umi/dist/commands/dev/plugins/ViteHtmlPlugin';
|
||||||
|
import { printMemoryUsage } from '@umijs/preset-umi/dist/commands/dev/printMemoryUsage';
|
||||||
|
import {
|
||||||
|
addUnWatch,
|
||||||
|
createDebouncedHandler,
|
||||||
|
expandCSSPaths,
|
||||||
|
expandJSPaths,
|
||||||
|
unwatch,
|
||||||
|
watch,
|
||||||
|
} from '@umijs/preset-umi/dist/commands/dev/watch';
|
||||||
|
import { DEFAULT_HOST, DEFAULT_PORT } from '@umijs/preset-umi/dist/constants';
|
||||||
|
import { LazySourceCodeCache } from '@umijs/preset-umi/dist/libs/folderCache/LazySourceCodeCache';
|
||||||
|
import type { GenerateFilesFn, IApi } from '@umijs/preset-umi/dist/types';
|
||||||
|
import { lazyImportFromCurrentPkg } from '@umijs/preset-umi/dist/utils/lazyImportFromCurrentPkg';
|
||||||
|
import {
|
||||||
|
address,
|
||||||
|
chalk,
|
||||||
|
lodash,
|
||||||
|
logger,
|
||||||
|
portfinder,
|
||||||
|
rimraf,
|
||||||
|
winPath,
|
||||||
|
} from '@umijs/utils';
|
||||||
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
||||||
|
import { basename, join } from 'path';
|
||||||
|
import { Worker } from 'worker_threads';
|
||||||
|
|
||||||
|
const bundlerWebpack: typeof import('@umijs/bundler-webpack') =
|
||||||
|
lazyImportFromCurrentPkg('@umijs/bundler-webpack');
|
||||||
|
const bundlerVite: typeof import('@umijs/bundler-vite') =
|
||||||
|
lazyImportFromCurrentPkg('@umijs/bundler-vite');
|
||||||
|
|
||||||
|
const MFSU_EAGER_DEFAULT_INCLUDE = [
|
||||||
|
'react',
|
||||||
|
'react-error-overlay',
|
||||||
|
'react/jsx-dev-runtime',
|
||||||
|
'@umijs/utils/compiled/strip-ansi',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
api.describe({
|
||||||
|
enableBy() {
|
||||||
|
return api.name === 'dev';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.registerCommand({
|
||||||
|
name: 'dev',
|
||||||
|
description: 'dev server for development',
|
||||||
|
details: `
|
||||||
|
inula dev
|
||||||
|
|
||||||
|
# dev with specified port
|
||||||
|
PORT=8888 inula dev
|
||||||
|
`,
|
||||||
|
async fn() {
|
||||||
|
logger.info(chalk.cyan.bold(`Inula v${api.appData.umi.version}`));
|
||||||
|
const enableVite = !!api.config.vite;
|
||||||
|
|
||||||
|
// clear tmp
|
||||||
|
rimraf.sync(api.paths.absTmpPath);
|
||||||
|
|
||||||
|
// check package.json
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'onCheckPkgJSON',
|
||||||
|
args: {
|
||||||
|
origin: null,
|
||||||
|
current: api.appData.pkg,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// clean cache if umi version not matched
|
||||||
|
// const umiJSONPath = join(api.paths.absTmpPath, 'umi.json');
|
||||||
|
// if (existsSync(umiJSONPath)) {
|
||||||
|
// const originVersion = require(umiJSONPath).version;
|
||||||
|
// if (originVersion !== api.appData.umi.version) {
|
||||||
|
// logger.info(`Delete cache folder since umi version updated.`);
|
||||||
|
// rimraf.sync(api.paths.absTmpPath);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// fsExtra.outputFileSync(
|
||||||
|
// umiJSONPath,
|
||||||
|
// JSON.stringify({ version: api.appData.umi.version }),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// generate files
|
||||||
|
const generate: GenerateFilesFn = async (opts) => {
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'onGenerateFiles',
|
||||||
|
args: {
|
||||||
|
files: opts.files || null,
|
||||||
|
isFirstTime: opts.isFirstTime,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
await generate({
|
||||||
|
isFirstTime: true,
|
||||||
|
});
|
||||||
|
const { absPagesPath, absSrcPath } = api.paths;
|
||||||
|
const watcherPaths: string[] = await api.applyPlugins({
|
||||||
|
key: 'addTmpGenerateWatcherPaths',
|
||||||
|
initialValue: [
|
||||||
|
absPagesPath,
|
||||||
|
!api.config.routes && api.config.conventionRoutes?.base,
|
||||||
|
join(absSrcPath, 'layouts'),
|
||||||
|
...expandJSPaths(join(absSrcPath, 'loading')),
|
||||||
|
...expandJSPaths(join(absSrcPath, 'app')),
|
||||||
|
...expandJSPaths(join(absSrcPath, 'global')),
|
||||||
|
...expandCSSPaths(join(absSrcPath, 'global')),
|
||||||
|
...expandCSSPaths(join(absSrcPath, 'overrides')),
|
||||||
|
].filter(Boolean),
|
||||||
|
});
|
||||||
|
lodash.uniq<string>(watcherPaths.map(winPath)).forEach((p: string) => {
|
||||||
|
watch({
|
||||||
|
path: p,
|
||||||
|
addToUnWatches: true,
|
||||||
|
onChange: createDebouncedHandler({
|
||||||
|
timeout: 2000,
|
||||||
|
async onChange(opts) {
|
||||||
|
await generate({ files: opts.files, isFirstTime: false });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// watch package.json change
|
||||||
|
const pkgPath = join(api.cwd, 'package.json');
|
||||||
|
watch({
|
||||||
|
path: pkgPath,
|
||||||
|
addToUnWatches: true,
|
||||||
|
onChange() {
|
||||||
|
// Why try catch?
|
||||||
|
// ref: https://github.com/umijs/umi/issues/8608
|
||||||
|
try {
|
||||||
|
const origin = api.appData.pkg;
|
||||||
|
api.appData.pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
||||||
|
api.applyPlugins({
|
||||||
|
key: 'onCheckPkgJSON',
|
||||||
|
args: {
|
||||||
|
origin,
|
||||||
|
current: api.appData.pkg,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
api.applyPlugins({
|
||||||
|
key: 'onPkgJSONChanged',
|
||||||
|
args: {
|
||||||
|
origin,
|
||||||
|
current: api.appData.pkg,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// watch config change
|
||||||
|
addUnWatch(
|
||||||
|
api.service.configManager!.watch({
|
||||||
|
schemas: api.service.configSchemas,
|
||||||
|
onChangeTypes: api.service.configOnChanges,
|
||||||
|
async onChange(opts) {
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'onCheckConfig',
|
||||||
|
args: {
|
||||||
|
config: api.config,
|
||||||
|
userConfig: api.userConfig,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const { data } = opts;
|
||||||
|
if (data.changes[api.ConfigChangeType.reload]) {
|
||||||
|
logger.event(
|
||||||
|
`config ${data.changes[api.ConfigChangeType.reload].join(
|
||||||
|
', ',
|
||||||
|
)} changed, restart server...`,
|
||||||
|
);
|
||||||
|
api.restartServer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await api.service.resolveConfig();
|
||||||
|
if (data.changes[api.ConfigChangeType.regenerateTmpFiles]) {
|
||||||
|
logger.event(
|
||||||
|
`config ${data.changes[
|
||||||
|
api.ConfigChangeType.regenerateTmpFiles
|
||||||
|
].join(', ')} changed, regenerate tmp files...`,
|
||||||
|
);
|
||||||
|
await generate({ isFirstTime: false });
|
||||||
|
}
|
||||||
|
for await (const fn of data.fns) {
|
||||||
|
await fn();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// watch plugin change
|
||||||
|
const pluginFiles: string[] = [
|
||||||
|
join(api.cwd, 'plugin.ts'),
|
||||||
|
join(api.cwd, 'plugin.js'),
|
||||||
|
];
|
||||||
|
pluginFiles.forEach((filePath: string) => {
|
||||||
|
watch({
|
||||||
|
path: filePath,
|
||||||
|
addToUnWatches: true,
|
||||||
|
onChange() {
|
||||||
|
logger.event(`${basename(filePath)} changed, restart server...`);
|
||||||
|
api.restartServer();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// watch public dir change and restart server
|
||||||
|
function watchPublicDirChange() {
|
||||||
|
const publicDir = join(api.cwd, 'public');
|
||||||
|
const isPublicAvailable =
|
||||||
|
existsSync(publicDir) && readdirSync(publicDir).length;
|
||||||
|
let restarted = false;
|
||||||
|
const restartServer = () => {
|
||||||
|
if (restarted) return;
|
||||||
|
restarted = true;
|
||||||
|
logger.event(`public dir changed, restart server...`);
|
||||||
|
api.restartServer();
|
||||||
|
};
|
||||||
|
watch({
|
||||||
|
path: publicDir,
|
||||||
|
addToUnWatches: true,
|
||||||
|
onChange(event, path) {
|
||||||
|
if (isPublicAvailable) {
|
||||||
|
// listen public dir delete event
|
||||||
|
if (event === 'unlinkDir' && path === publicDir) {
|
||||||
|
restartServer();
|
||||||
|
} else if (
|
||||||
|
// listen public files all deleted
|
||||||
|
event === 'unlink' &&
|
||||||
|
existsSync(publicDir) &&
|
||||||
|
readdirSync(publicDir).length === 0
|
||||||
|
) {
|
||||||
|
restartServer();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// listen public dir add first file event
|
||||||
|
if (
|
||||||
|
event === 'add' &&
|
||||||
|
existsSync(publicDir) &&
|
||||||
|
readdirSync(publicDir).length === 1
|
||||||
|
) {
|
||||||
|
restartServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
watchPublicDirChange();
|
||||||
|
|
||||||
|
// start dev server
|
||||||
|
const beforeMiddlewares = await api.applyPlugins({
|
||||||
|
key: 'addBeforeMiddlewares',
|
||||||
|
initialValue: [],
|
||||||
|
});
|
||||||
|
const middlewares = await api.applyPlugins({
|
||||||
|
key: 'addMiddlewares',
|
||||||
|
initialValue: [],
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
babelPreset,
|
||||||
|
beforeBabelPlugins,
|
||||||
|
beforeBabelPresets,
|
||||||
|
extraBabelPlugins,
|
||||||
|
extraBabelPresets,
|
||||||
|
} = await getBabelOpts({ api });
|
||||||
|
const chainWebpack = async (memo: any, args: Object) => {
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'chainWebpack',
|
||||||
|
type: api.ApplyPluginsType.modify,
|
||||||
|
initialValue: memo,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const modifyWebpackConfig = async (memo: any, args: Object) => {
|
||||||
|
return await api.applyPlugins({
|
||||||
|
key: 'modifyWebpackConfig',
|
||||||
|
initialValue: memo,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const modifyViteConfig = async (memo: any, args: Object) => {
|
||||||
|
return await api.applyPlugins({
|
||||||
|
key: 'modifyViteConfig',
|
||||||
|
initialValue: memo,
|
||||||
|
args,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const debouncedPrintMemoryUsage = lodash.debounce(printMemoryUsage, 5000);
|
||||||
|
|
||||||
|
let srcCodeCache: LazySourceCodeCache | undefined;
|
||||||
|
let startBuildWorker: (deps: any[]) => Worker = (() => {}) as any;
|
||||||
|
|
||||||
|
const entry = await api.applyPlugins({
|
||||||
|
key: 'modifyEntry',
|
||||||
|
initialValue: {
|
||||||
|
umi: join(api.paths.absTmpPath, 'umi.ts'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const opts: any = {
|
||||||
|
config: api.config,
|
||||||
|
pkg: api.pkg,
|
||||||
|
cwd: api.cwd,
|
||||||
|
rootDir: process.cwd(),
|
||||||
|
entry,
|
||||||
|
port: api.appData.port,
|
||||||
|
host: api.appData.host,
|
||||||
|
ip: api.appData.ip,
|
||||||
|
...(enableVite
|
||||||
|
? { modifyViteConfig }
|
||||||
|
: { babelPreset, chainWebpack, modifyWebpackConfig }),
|
||||||
|
beforeBabelPlugins,
|
||||||
|
beforeBabelPresets,
|
||||||
|
extraBabelPlugins,
|
||||||
|
extraBabelPresets,
|
||||||
|
beforeMiddlewares: ([] as RequestHandler[]).concat([
|
||||||
|
...beforeMiddlewares,
|
||||||
|
]),
|
||||||
|
// vite 模式使用 ./plugins/ViteHtmlPlugin.ts 处理
|
||||||
|
afterMiddlewares: enableVite
|
||||||
|
? [middlewares.concat(faviconMiddleware)]
|
||||||
|
: middlewares.concat([
|
||||||
|
...(api.config.mpa ? [] : [createRouteMiddleware({ api })]),
|
||||||
|
// 放置 favicon 在 webpack middleware 之后,兼容 public 目录下有 favicon.ico 的场景
|
||||||
|
// ref: https://github.com/umijs/umi/issues/8024
|
||||||
|
faviconMiddleware,
|
||||||
|
]),
|
||||||
|
onDevCompileDone(opts: any) {
|
||||||
|
debouncedPrintMemoryUsage;
|
||||||
|
// debouncedPrintMemoryUsage();
|
||||||
|
api.appData.bundleStatus.done = true;
|
||||||
|
api.applyPlugins({
|
||||||
|
key: 'onDevCompileDone',
|
||||||
|
args: opts,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onProgress(opts: any) {
|
||||||
|
api.appData.bundleStatus.progresses = opts.progresses;
|
||||||
|
},
|
||||||
|
onMFSUProgress(opts: any) {
|
||||||
|
api.appData.mfsuBundleStatus = {
|
||||||
|
...api.appData.mfsuBundleStatus,
|
||||||
|
...opts,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mfsuWithESBuild: api.config.mfsu?.esbuild,
|
||||||
|
mfsuStrategy: api.config.mfsu?.strategy,
|
||||||
|
cache: {
|
||||||
|
buildDependencies: [
|
||||||
|
api.pkgPath,
|
||||||
|
api.service.configManager!.mainConfigFile || '',
|
||||||
|
].filter(Boolean),
|
||||||
|
},
|
||||||
|
srcCodeCache,
|
||||||
|
mfsuInclude: lodash.union([
|
||||||
|
...MFSU_EAGER_DEFAULT_INCLUDE,
|
||||||
|
...(api.config.mfsu?.include || []),
|
||||||
|
]),
|
||||||
|
startBuildWorker,
|
||||||
|
onBeforeMiddleware(app: any) {
|
||||||
|
api.applyPlugins({
|
||||||
|
key: 'onBeforeMiddleware',
|
||||||
|
args: {
|
||||||
|
app,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'onBeforeCompiler',
|
||||||
|
args: { compiler: enableVite ? 'vite' : 'webpack', opts },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (enableVite) {
|
||||||
|
await bundlerVite.dev(opts);
|
||||||
|
} else if (process.env.OKAM) {
|
||||||
|
require('@umijs/bundler-webpack/dist/requireHook');
|
||||||
|
const { dev } = require(process.env.OKAM);
|
||||||
|
await dev(opts);
|
||||||
|
} else {
|
||||||
|
await bundlerWebpack.dev(opts);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.modifyAppData(async (memo) => {
|
||||||
|
memo.port = await portfinder.getPortPromise({
|
||||||
|
port: parseInt(String(process.env.PORT || DEFAULT_PORT), 10),
|
||||||
|
});
|
||||||
|
memo.host = process.env.HOST || DEFAULT_HOST;
|
||||||
|
memo.ip = address.ip();
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.registerMethod({
|
||||||
|
name: 'restartServer',
|
||||||
|
fn() {
|
||||||
|
logger.info(`Restart dev server with port ${api.appData.port}...`);
|
||||||
|
unwatch();
|
||||||
|
|
||||||
|
process.send?.({
|
||||||
|
type: 'RESTART',
|
||||||
|
payload: {
|
||||||
|
port: api.appData.port,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.modifyViteConfig((viteConfig) => {
|
||||||
|
viteConfig.plugins?.push(ViteHtmlPlugin(api));
|
||||||
|
return viteConfig;
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,74 @@
|
||||||
|
import type { IApi } from '@umijs/preset-umi/dist/types';
|
||||||
|
import { chalk, lodash, logger } from '@umijs/utils';
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
api.registerCommand({
|
||||||
|
name: 'help',
|
||||||
|
description: 'show commands help',
|
||||||
|
details: `
|
||||||
|
inula help build
|
||||||
|
inula help dev
|
||||||
|
`,
|
||||||
|
configResolveMode: 'loose',
|
||||||
|
fn() {
|
||||||
|
const subCommand = api.args._[0];
|
||||||
|
if (subCommand) {
|
||||||
|
if (subCommand in api.service.commands) {
|
||||||
|
showHelp(api.service.commands[subCommand]);
|
||||||
|
} else {
|
||||||
|
logger.error(`Invalid sub command ${subCommand}.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showHelps(api.service.commands);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function showHelp(command: any) {
|
||||||
|
console.log(`
|
||||||
|
Usage: inula ${command.name} [options]
|
||||||
|
${command.description ? `${chalk.gray(command.description)}.\n` : ''}
|
||||||
|
${command.options ? `Options:\n${padLeft(command.options)}\n` : ''}
|
||||||
|
${command.details ? `Details:\n${padLeft(command.details)}` : ''}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHelps(commands: typeof api.service.commands) {
|
||||||
|
console.log(`
|
||||||
|
Usage: inula <command> [options]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
|
||||||
|
${getDeps(commands)}
|
||||||
|
`);
|
||||||
|
console.log(
|
||||||
|
`Run \`${chalk.bold(
|
||||||
|
'inula help <command>',
|
||||||
|
)}\` for more information of specific commands.`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`Visit ${chalk.bold(
|
||||||
|
'https://docs.openinula.net/',
|
||||||
|
)} to learn more about Inula.`,
|
||||||
|
);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeps(commands: any) {
|
||||||
|
return Object.keys(commands)
|
||||||
|
.map((key) => {
|
||||||
|
return ` ${chalk.green(lodash.padEnd(key, 10))}${
|
||||||
|
commands[key].description || ''
|
||||||
|
}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function padLeft(str: string) {
|
||||||
|
return str
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.map((line: string) => ` ${line}`)
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { IApi } from '@umijs/preset-umi';
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
api.registerCommand({
|
||||||
|
name: 'version',
|
||||||
|
alias: 'v',
|
||||||
|
description: 'show inula version',
|
||||||
|
configResolveMode: 'loose',
|
||||||
|
fn({ args }) {
|
||||||
|
const version = require('../../../../../package.json').version;
|
||||||
|
if (!args.quiet) {
|
||||||
|
console.log(`inula@${version}`);
|
||||||
|
}
|
||||||
|
return version;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { IApi } from '@umijs/preset-umi';
|
||||||
|
import { copyFileSync } from 'fs';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { DEFAULT_FAVICON_FILE, DEFAULT_FAVICON_FILE_NAME } from '../constants';
|
||||||
|
import { resolveProjectDep } from '../utils/resolveProjectDep';
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
const version = require('../../../../../package.json').version;
|
||||||
|
const inulaPath =
|
||||||
|
resolveProjectDep({
|
||||||
|
pkg: api.pkg,
|
||||||
|
cwd: api.cwd,
|
||||||
|
dep: 'openinula',
|
||||||
|
}) || dirname(require.resolve('openinula/package.json'));
|
||||||
|
|
||||||
|
const openAPI = api.userConfig?.openAPI ?? [];
|
||||||
|
const configDefaults: Record<string, any> = {
|
||||||
|
mfsu: false,
|
||||||
|
// 开发使用而已,能力可以有 inula 提供 aigc
|
||||||
|
azure: {
|
||||||
|
apiVersion: '2023-07-01-preview',
|
||||||
|
model: 'alita4',
|
||||||
|
resource: 'alita',
|
||||||
|
},
|
||||||
|
...api.userConfig,
|
||||||
|
openAPI: openAPI.map((i: any) => ({
|
||||||
|
requestLibPath: "import { ir as request } from 'inula'",
|
||||||
|
...i,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
api.modifyAppData((memo) => {
|
||||||
|
memo.umi.name = 'Inula';
|
||||||
|
memo.umi.importSource = 'inula';
|
||||||
|
memo.umi.cliName = 'inula';
|
||||||
|
memo.umi.version = version;
|
||||||
|
memo.openinula ??= {};
|
||||||
|
memo.openinula.path = inulaPath;
|
||||||
|
memo.openinula.version = require('openinula/package.json').version;
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.addBeforeMiddlewares(() => [
|
||||||
|
(req, res, next) => {
|
||||||
|
// 开发的时候,用户没有设置 favicon ,我们塞了一个
|
||||||
|
if (
|
||||||
|
!(req.path === `${api.config.publicPath}${DEFAULT_FAVICON_FILE_NAME}`)
|
||||||
|
) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.sendFile(DEFAULT_FAVICON_FILE);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.modifyHTMLFavicon((memo) => {
|
||||||
|
// 用户没有设置,要赛一个
|
||||||
|
if (!api.appData.faviconFiles || !api.appData.faviconFiles.length) {
|
||||||
|
memo.push(`${api.config.publicPath}${DEFAULT_FAVICON_FILE_NAME}`);
|
||||||
|
}
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onBuildComplete(({ err }) => {
|
||||||
|
if (err) return;
|
||||||
|
// 用户没有设置,要拷贝一个
|
||||||
|
if (!api.appData.faviconFiles || !api.appData.faviconFiles.length) {
|
||||||
|
copyFileSync(
|
||||||
|
DEFAULT_FAVICON_FILE,
|
||||||
|
join(api.paths.absOutputPath, DEFAULT_FAVICON_FILE_NAME),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
api.modifyDefaultConfig((memo: any) => {
|
||||||
|
Object.keys(configDefaults).forEach((key) => {
|
||||||
|
if (key === 'alias') {
|
||||||
|
memo[key] = { ...memo[key], ...configDefaults[key] };
|
||||||
|
} else {
|
||||||
|
memo[key] = configDefaults[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
memo.alias.inula = '@@/exports';
|
||||||
|
memo.alias.openinula = inulaPath;
|
||||||
|
memo.alias.react = inulaPath;
|
||||||
|
memo.alias.openinula = inulaPath;
|
||||||
|
memo.alias['react-dom'] = inulaPath;
|
||||||
|
// react-dom/client 顺序要在 react-dom 之前
|
||||||
|
if (memo.alias['react-dom/client']) {
|
||||||
|
memo.alias['react-dom/client'] = inulaPath;
|
||||||
|
} else {
|
||||||
|
memo.alias = {
|
||||||
|
'react-dom/client': inulaPath,
|
||||||
|
...memo.alias,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// umi4 开发环境不允许配置为 './'
|
||||||
|
if (process.env.NODE_ENV === 'development' && memo.publicPath === './') {
|
||||||
|
console.warn('开发环境不允许配置为 "./"');
|
||||||
|
memo.publicPath = '/';
|
||||||
|
}
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.modifyBabelPresetOpts((memo) => {
|
||||||
|
memo.presetReact = {
|
||||||
|
runtime: 'automatic',
|
||||||
|
importSource: 'openinula',
|
||||||
|
};
|
||||||
|
return memo;
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
export const DEFAULT_FAVICON_FILE_NAME = 'favicon.ico';
|
||||||
|
export const DEFAULT_FAVICON_FILE = join(__dirname, DEFAULT_FAVICON_FILE_NAME);
|
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
|
@ -0,0 +1,16 @@
|
||||||
|
import { IApi } from '@aluni/types';
|
||||||
|
import { cheerio } from '@umijs/utils';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const assetsDir = join(__dirname, '../../assets');
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
api.register({
|
||||||
|
key: 'modifyDevToolLoadingHTML',
|
||||||
|
fn: () =>
|
||||||
|
cheerio.load(
|
||||||
|
readFileSync(join(assetsDir, 'bundle-status.html'), 'utf-8'),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,752 @@
|
||||||
|
import { IApi } from '@umijs/preset-umi';
|
||||||
|
import { TEMPLATES_DIR } from '@umijs/preset-umi/dist/constants';
|
||||||
|
import { getModuleExports } from '@umijs/preset-umi/dist/features/tmpFiles/getModuleExports';
|
||||||
|
import { importsToStr } from '@umijs/preset-umi/dist/features/tmpFiles/importsToStr';
|
||||||
|
import { importLazy, lodash, winPath } from '@umijs/utils';
|
||||||
|
import { existsSync, readdirSync } from 'fs';
|
||||||
|
import { basename, dirname, join } from 'path';
|
||||||
|
|
||||||
|
const RUNTIME_TYPE_FILE_NAME = 'runtimeConfig.d.ts';
|
||||||
|
|
||||||
|
const routesApi: typeof import('@umijs/preset-umi/dist/features/tmpFiles/routes') =
|
||||||
|
importLazy(
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/tmpFiles/routes'),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default (api: IApi) => {
|
||||||
|
const umiDir = process.env.UMI_DIR!;
|
||||||
|
|
||||||
|
api.describe({
|
||||||
|
key: 'tmpFiles',
|
||||||
|
config: {
|
||||||
|
schema({ zod }) {
|
||||||
|
return zod.boolean();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
api.onGenerateFiles(async (opts) => {
|
||||||
|
const rendererPath = winPath(
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'modifyRendererPath',
|
||||||
|
initialValue: dirname(
|
||||||
|
require.resolve('@umijs/renderer-react/package.json'),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const serverRendererPath = winPath(
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'modifyServerRendererPath',
|
||||||
|
initialValue: join(rendererPath, 'dist/server.js'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// tsconfig.json
|
||||||
|
const frameworkName = api.service.frameworkName;
|
||||||
|
const srcPrefix = api.appData.hasSrcDir ? 'src/' : '';
|
||||||
|
const umiTempDir = `${srcPrefix}.${frameworkName}`;
|
||||||
|
const baseUrl = api.appData.hasSrcDir ? '../../' : '../';
|
||||||
|
const isTs5 = api.appData.typescript.tsVersion?.startsWith('5');
|
||||||
|
const isTslibInstalled = !!api.appData.typescript.tslibVersion;
|
||||||
|
|
||||||
|
// x 1、basic config
|
||||||
|
// x 2、alias
|
||||||
|
// 3、language service platform
|
||||||
|
// 4、typing
|
||||||
|
let umiTsConfig = {
|
||||||
|
compilerOptions: {
|
||||||
|
target: 'esnext',
|
||||||
|
module: 'esnext',
|
||||||
|
lib: ['dom', 'dom.iterable', 'esnext'],
|
||||||
|
allowJs: true,
|
||||||
|
skipLibCheck: true,
|
||||||
|
moduleResolution: isTs5 ? 'bundler' : 'node',
|
||||||
|
importHelpers: isTslibInstalled,
|
||||||
|
noEmit: true,
|
||||||
|
// jsx: api.appData.framework === 'vue' ? 'preserve' : 'react-jsx',
|
||||||
|
// openinula 用的也是 preserve
|
||||||
|
jsx: 'preserve',
|
||||||
|
esModuleInterop: true,
|
||||||
|
sourceMap: true,
|
||||||
|
baseUrl,
|
||||||
|
strict: true,
|
||||||
|
resolveJsonModule: true,
|
||||||
|
allowSyntheticDefaultImports: true,
|
||||||
|
|
||||||
|
// Supported by vue only
|
||||||
|
...(api.appData.framework === 'vue'
|
||||||
|
? {
|
||||||
|
// TODO Actually, it should be vite mode, but here it is written as vue only
|
||||||
|
// Required in Vite https://vitejs.dev/guide/features.html#typescript
|
||||||
|
isolatedModules: true,
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
|
||||||
|
paths: {
|
||||||
|
'@/*': [`${srcPrefix}*`],
|
||||||
|
'@@/*': [`${umiTempDir}/*`],
|
||||||
|
openinula: [api.appData.openinula.path],
|
||||||
|
[`${api.appData.umi.importSource}`]: [umiDir],
|
||||||
|
[`${api.appData.umi.importSource}/typings`]: [
|
||||||
|
`${umiTempDir}/typings`,
|
||||||
|
],
|
||||||
|
...(api.config.vite
|
||||||
|
? {
|
||||||
|
'@fs/*': ['*'],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
`${baseUrl}.${frameworkName}rc.ts`,
|
||||||
|
`${baseUrl}**/*.d.ts`,
|
||||||
|
`${baseUrl}**/*.ts`,
|
||||||
|
`${baseUrl}**/*.tsx`,
|
||||||
|
api.appData.framework === 'vue' && `${baseUrl}**/*.vue`,
|
||||||
|
].filter(Boolean),
|
||||||
|
};
|
||||||
|
|
||||||
|
umiTsConfig = await api.applyPlugins({
|
||||||
|
key: 'modifyTSConfig',
|
||||||
|
type: api.ApplyPluginsType.modify,
|
||||||
|
initialValue: umiTsConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
api.writeTmpFile({
|
||||||
|
noPluginDir: true,
|
||||||
|
path: 'tsconfig.json',
|
||||||
|
content: JSON.stringify(umiTsConfig, null, 2),
|
||||||
|
});
|
||||||
|
|
||||||
|
// typings.d.ts
|
||||||
|
// ref: https://github.com/vitejs/vite/blob/main/packages/vite/client.d.ts
|
||||||
|
api.writeTmpFile({
|
||||||
|
noPluginDir: true,
|
||||||
|
path: 'typings.d.ts',
|
||||||
|
content: `
|
||||||
|
type CSSModuleClasses = { readonly [key: string]: string }
|
||||||
|
declare module '*.css' {
|
||||||
|
const classes: CSSModuleClasses
|
||||||
|
export default classes
|
||||||
|
}
|
||||||
|
declare module '*.scss' {
|
||||||
|
const classes: CSSModuleClasses
|
||||||
|
export default classes
|
||||||
|
}
|
||||||
|
declare module '*.sass' {
|
||||||
|
const classes: CSSModuleClasses
|
||||||
|
export default classes
|
||||||
|
}
|
||||||
|
declare module '*.less' {
|
||||||
|
const classes: CSSModuleClasses
|
||||||
|
export default classes
|
||||||
|
}
|
||||||
|
declare module '*.styl' {
|
||||||
|
const classes: CSSModuleClasses
|
||||||
|
export default classes
|
||||||
|
}
|
||||||
|
declare module '*.stylus' {
|
||||||
|
const classes: CSSModuleClasses
|
||||||
|
export default classes
|
||||||
|
}
|
||||||
|
|
||||||
|
// images
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.png' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.gif' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.svg' {
|
||||||
|
${
|
||||||
|
api.config.svgr
|
||||||
|
? `
|
||||||
|
import * as React from 'react';
|
||||||
|
export const ReactComponent: React.FunctionComponent<React.SVGProps<
|
||||||
|
SVGSVGElement
|
||||||
|
> & { title?: string }>;
|
||||||
|
`.trimStart()
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.ico' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.webp' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.avif' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
|
||||||
|
// media
|
||||||
|
declare module '*.mp4' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.webm' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.ogg' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.mp3' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.wav' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.flac' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.aac' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
|
||||||
|
// fonts
|
||||||
|
declare module '*.woff' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.woff2' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.eot' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.ttf' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.otf' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
|
||||||
|
// other
|
||||||
|
declare module '*.wasm' {
|
||||||
|
const initWasm: (options: WebAssembly.Imports) => Promise<WebAssembly.Exports>
|
||||||
|
export default initWasm
|
||||||
|
}
|
||||||
|
declare module '*.webmanifest' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.pdf' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
declare module '*.txt' {
|
||||||
|
const src: string
|
||||||
|
export default src
|
||||||
|
}
|
||||||
|
`.trimEnd(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// umi.ts
|
||||||
|
api.writeTmpFile({
|
||||||
|
noPluginDir: true,
|
||||||
|
path: 'umi.ts',
|
||||||
|
tplPath: join(TEMPLATES_DIR, 'umi.tpl'),
|
||||||
|
context: {
|
||||||
|
mountElementId: api.config.mountElementId,
|
||||||
|
rendererPath,
|
||||||
|
publicPath: api.config.publicPath,
|
||||||
|
runtimePublicPath: api.config.runtimePublicPath ? 'true' : 'false',
|
||||||
|
entryCode: (
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'addEntryCode',
|
||||||
|
initialValue: [],
|
||||||
|
})
|
||||||
|
).join('\n'),
|
||||||
|
entryCodeAhead: (
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'addEntryCodeAhead',
|
||||||
|
initialValue: [],
|
||||||
|
})
|
||||||
|
).join('\n'),
|
||||||
|
polyfillImports: importsToStr(
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'addPolyfillImports',
|
||||||
|
initialValue: [],
|
||||||
|
}),
|
||||||
|
).join('\n'),
|
||||||
|
importsAhead: importsToStr(
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'addEntryImportsAhead',
|
||||||
|
initialValue: [
|
||||||
|
api.appData.globalCSS.length && {
|
||||||
|
source: api.appData.globalCSS[0],
|
||||||
|
},
|
||||||
|
api.appData.globalJS.length && {
|
||||||
|
source: api.appData.globalJS[0],
|
||||||
|
},
|
||||||
|
].filter(Boolean),
|
||||||
|
}),
|
||||||
|
).join('\n'),
|
||||||
|
imports: importsToStr(
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'addEntryImports',
|
||||||
|
initialValue: [],
|
||||||
|
}),
|
||||||
|
).join('\n'),
|
||||||
|
basename: api.config.base,
|
||||||
|
historyType: api.config.history.type,
|
||||||
|
hydrate: !!api.config.ssr,
|
||||||
|
reactRouter5Compat: !!api.config.reactRouter5Compat,
|
||||||
|
loadingComponent: api.appData.globalLoading,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// EmptyRoute.tsx
|
||||||
|
api.writeTmpFile({
|
||||||
|
noPluginDir: true,
|
||||||
|
path: 'core/EmptyRoute.tsx',
|
||||||
|
// https://github.com/umijs/umi/issues/8782
|
||||||
|
// Empty <Outlet /> needs to pass through outlet context, otherwise nested route will not get context value.
|
||||||
|
content: `
|
||||||
|
import React from 'react';
|
||||||
|
import { Outlet, useOutletContext } from 'umi';
|
||||||
|
export default function EmptyRoute() {
|
||||||
|
const context = useOutletContext();
|
||||||
|
return <Outlet context={context} />;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// route.ts
|
||||||
|
let routes: any;
|
||||||
|
if (opts.isFirstTime) {
|
||||||
|
routes = api.appData.routes;
|
||||||
|
} else {
|
||||||
|
routes = await routesApi.getRoutes({
|
||||||
|
api,
|
||||||
|
});
|
||||||
|
// refresh route data, prevent route data outdated
|
||||||
|
// this can immediately get the latest `icon`... props in routes config
|
||||||
|
api.appData.routes = routes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSrc = api.appData.hasSrcDir;
|
||||||
|
// @/pages/
|
||||||
|
const pages = basename(
|
||||||
|
api.config.conventionRoutes?.base || api.paths.absPagesPath,
|
||||||
|
);
|
||||||
|
const prefix = hasSrc ? `../../../src/${pages}/` : `../../${pages}/`;
|
||||||
|
const clonedRoutes = lodash.cloneDeep(routes);
|
||||||
|
for (const id of Object.keys(clonedRoutes)) {
|
||||||
|
for (const key of Object.keys(clonedRoutes[id])) {
|
||||||
|
const route = clonedRoutes[id];
|
||||||
|
// Remove __ prefix props, absPath props and file props
|
||||||
|
if (key.startsWith('__') || ['absPath', 'file'].includes(key)) {
|
||||||
|
delete route[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// header imports
|
||||||
|
const headerImports: string[] = [];
|
||||||
|
|
||||||
|
// trim quotes
|
||||||
|
let routesString = JSON.stringify(clonedRoutes);
|
||||||
|
if (api.config.clientLoader) {
|
||||||
|
// "clientLoaders['foo']" > clientLoaders['foo']
|
||||||
|
routesString = routesString.replace(/"(clientLoaders\[.*?)"/g, '$1');
|
||||||
|
// import: client loader
|
||||||
|
headerImports.push(`import clientLoaders from './loaders.js';`);
|
||||||
|
}
|
||||||
|
// routeProps is enabled for conventional routes
|
||||||
|
// e.g. dumi 需要用到约定式路由但又不需要 routeProps
|
||||||
|
if (!api.userConfig.routes && api.isPluginEnable('routeProps')) {
|
||||||
|
// routeProps":"routeProps['foo']" > ...routeProps['foo']
|
||||||
|
routesString = routesString.replace(
|
||||||
|
/"routeProps":"(routeProps\[.*?)"/g,
|
||||||
|
'...$1',
|
||||||
|
);
|
||||||
|
// import: route props
|
||||||
|
headerImports.push(`import routeProps from './routeProps';`);
|
||||||
|
// prevent override internal route props
|
||||||
|
headerImports.push(`
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
Object.entries(routeProps).forEach(([key, value]) => {
|
||||||
|
const internalProps = ['path', 'id', 'parentId', 'isLayout', 'isWrapper', 'layout', 'clientLoader'];
|
||||||
|
Object.keys(value).forEach((prop) => {
|
||||||
|
if (internalProps.includes(prop)) {
|
||||||
|
throw new Error(
|
||||||
|
\`[UmiJS] route '\${key}' should not have '\${prop}' prop, please remove this property in 'routeProps'.\`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// import: react
|
||||||
|
if (api.appData.framework === 'react') {
|
||||||
|
headerImports.push(`import React from 'react';`);
|
||||||
|
}
|
||||||
|
|
||||||
|
api.writeTmpFile({
|
||||||
|
noPluginDir: true,
|
||||||
|
path: 'core/route.tsx',
|
||||||
|
tplPath: join(TEMPLATES_DIR, 'route.tpl'),
|
||||||
|
context: {
|
||||||
|
headerImports: headerImports.join('\n'),
|
||||||
|
routes: routesString,
|
||||||
|
routeComponents: await routesApi.getRouteComponents({
|
||||||
|
routes,
|
||||||
|
prefix,
|
||||||
|
api,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// plugin.ts
|
||||||
|
const plugins: string[] = await api.applyPlugins({
|
||||||
|
key: 'addRuntimePlugin',
|
||||||
|
initialValue: [api.appData.appJS?.path].filter(Boolean),
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkDuplicatePluginKeys(arr: string[]) {
|
||||||
|
const duplicates: string[] = [];
|
||||||
|
arr.reduce<Record<string, boolean>>((prev, curr) => {
|
||||||
|
if (prev[curr]) {
|
||||||
|
duplicates.push(curr);
|
||||||
|
} else {
|
||||||
|
prev[curr] = true;
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, {});
|
||||||
|
if (duplicates.length) {
|
||||||
|
throw new Error(
|
||||||
|
`The plugin key cannot be duplicated. (${duplicates.join(', ')})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validKeys = await api.applyPlugins({
|
||||||
|
key: 'addRuntimePluginKey',
|
||||||
|
initialValue: [
|
||||||
|
'patchRoutes',
|
||||||
|
'patchClientRoutes',
|
||||||
|
'modifyContextOpts',
|
||||||
|
'modifyClientRenderOpts',
|
||||||
|
'rootContainer',
|
||||||
|
'innerProvider',
|
||||||
|
'i18nProvider',
|
||||||
|
'accessProvider',
|
||||||
|
'dataflowProvider',
|
||||||
|
'outerProvider',
|
||||||
|
'render',
|
||||||
|
'onRouteChange',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
checkDuplicatePluginKeys(validKeys);
|
||||||
|
|
||||||
|
const appPluginRegExp = /(\/|\\)app.(ts|tsx|jsx|js)$/;
|
||||||
|
api.writeTmpFile({
|
||||||
|
noPluginDir: true,
|
||||||
|
path: 'core/plugin.ts',
|
||||||
|
tplPath: join(TEMPLATES_DIR, 'plugin.tpl'),
|
||||||
|
context: {
|
||||||
|
plugins: plugins.map((plugin, index) => ({
|
||||||
|
index,
|
||||||
|
// 在 app.ts 中,如果使用了 defineApp 方法,会存在 export default 的情况
|
||||||
|
hasDefaultExport: appPluginRegExp.test(plugin),
|
||||||
|
path: winPath(plugin),
|
||||||
|
})),
|
||||||
|
validKeys,
|
||||||
|
// Inject code for vite only
|
||||||
|
isViteMode: !!api.config.vite,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// umi.server.ts
|
||||||
|
if (api.config.ssr) {
|
||||||
|
const umiPluginPath = winPath(join(umiDir, 'client/client/plugin.js'));
|
||||||
|
const umiServerPath = winPath(require.resolve('@umijs/server/dist/ssr'));
|
||||||
|
const routesWithServerLoader = Object.keys(routes).reduce<
|
||||||
|
{ id: string; path: string }[]
|
||||||
|
>((memo, id) => {
|
||||||
|
if (routes[id].hasServerLoader) {
|
||||||
|
memo.push({
|
||||||
|
id,
|
||||||
|
path: routes[id].__absFile,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return memo;
|
||||||
|
}, []);
|
||||||
|
api.writeTmpFile({
|
||||||
|
noPluginDir: true,
|
||||||
|
path: 'umi.server.ts',
|
||||||
|
tplPath: join(TEMPLATES_DIR, 'server.tpl'),
|
||||||
|
context: {
|
||||||
|
routes: JSON.stringify(clonedRoutes, null, 2).replace(
|
||||||
|
/"component": "await import\((.*)\)"/g,
|
||||||
|
'"component": await import("$1")',
|
||||||
|
),
|
||||||
|
routesWithServerLoader,
|
||||||
|
umiPluginPath,
|
||||||
|
serverRendererPath,
|
||||||
|
umiServerPath,
|
||||||
|
validKeys,
|
||||||
|
assetsPath: winPath(
|
||||||
|
join(api.paths.absOutputPath, 'build-manifest.json'),
|
||||||
|
),
|
||||||
|
env: JSON.stringify(api.env),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// history.ts
|
||||||
|
// only react generates because the preset-vue override causes vite hot updates to fail
|
||||||
|
if (api.appData.framework === 'react') {
|
||||||
|
const { historyWithQuery, reactRouter5Compat } = api.config;
|
||||||
|
const historyPath = historyWithQuery
|
||||||
|
? winPath(dirname(require.resolve('@umijs/history/package.json')))
|
||||||
|
: rendererPath;
|
||||||
|
api.writeTmpFile({
|
||||||
|
noPluginDir: true,
|
||||||
|
path: 'core/history.ts',
|
||||||
|
tplPath: join(TEMPLATES_DIR, 'history.tpl'),
|
||||||
|
context: {
|
||||||
|
historyPath,
|
||||||
|
reactRouter5Compat,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
api.writeTmpFile({
|
||||||
|
noPluginDir: true,
|
||||||
|
path: 'core/historyIntelli.ts',
|
||||||
|
tplPath: join(TEMPLATES_DIR, 'historyIntelli.tpl'),
|
||||||
|
context: {
|
||||||
|
historyPath,
|
||||||
|
reactRouter5Compat,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkMembers(opts: {
|
||||||
|
path: string;
|
||||||
|
members: string[];
|
||||||
|
exportMembers: string[];
|
||||||
|
}) {
|
||||||
|
const conflicts = lodash.intersection(opts.exportMembers, opts.members);
|
||||||
|
if (conflicts.length) {
|
||||||
|
throw new Error(
|
||||||
|
`Conflict members: ${conflicts.join(', ')} in ${opts.path}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getExportsAndCheck(opts: {
|
||||||
|
path: string;
|
||||||
|
exportMembers: string[];
|
||||||
|
}) {
|
||||||
|
const members = (await getModuleExports({ file: opts.path })) as string[];
|
||||||
|
checkMembers({
|
||||||
|
members,
|
||||||
|
exportMembers: opts.exportMembers,
|
||||||
|
path: opts.path,
|
||||||
|
});
|
||||||
|
opts.exportMembers.push(...members);
|
||||||
|
return members;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate @@/exports.ts
|
||||||
|
api.register({
|
||||||
|
key: 'onGenerateFiles',
|
||||||
|
fn: async () => {
|
||||||
|
const rendererPath = winPath(
|
||||||
|
await api.applyPlugins({
|
||||||
|
key: 'modifyRendererPath',
|
||||||
|
initialValue: dirname(
|
||||||
|
require.resolve('@umijs/renderer-react/package.json'),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const exports = [];
|
||||||
|
const exportMembers = ['default'];
|
||||||
|
// @umijs/renderer-react
|
||||||
|
exports.push('// @umijs/renderer-*');
|
||||||
|
|
||||||
|
exports.push(
|
||||||
|
`export { ${(
|
||||||
|
await getExportsAndCheck({
|
||||||
|
path: join(rendererPath, 'dist/index.js'),
|
||||||
|
exportMembers,
|
||||||
|
})
|
||||||
|
).join(', ')} } from '${rendererPath}';`,
|
||||||
|
);
|
||||||
|
exports.push(`export type { History } from '${rendererPath}'`);
|
||||||
|
// umi/client/client/plugin
|
||||||
|
exports.push('// umi/client/client/plugin');
|
||||||
|
const umiPluginPath = winPath(join(umiDir, 'client/client/plugin.js'));
|
||||||
|
exports.push(
|
||||||
|
`export { ${(
|
||||||
|
await getExportsAndCheck({
|
||||||
|
path: umiPluginPath,
|
||||||
|
exportMembers,
|
||||||
|
})
|
||||||
|
).join(', ')} } from '${umiPluginPath}';`,
|
||||||
|
);
|
||||||
|
// @@/core/history.ts
|
||||||
|
exports.push(`export { history, createHistory } from './core/history';`);
|
||||||
|
checkMembers({
|
||||||
|
members: ['history', 'createHistory'],
|
||||||
|
exportMembers,
|
||||||
|
path: '@@/core/history.ts',
|
||||||
|
});
|
||||||
|
// @@/core/terminal.ts
|
||||||
|
if (api.service.config.terminal !== false) {
|
||||||
|
exports.push(`export { terminal } from './core/terminal';`);
|
||||||
|
checkMembers({
|
||||||
|
members: ['terminal'],
|
||||||
|
exportMembers,
|
||||||
|
path: '@@/core/terminal.ts',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (api.config.test !== false && api.appData.framework === 'react') {
|
||||||
|
if (
|
||||||
|
process.env.NODE_ENV === 'test' ||
|
||||||
|
// development is for TestBrowser's type
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
|
) {
|
||||||
|
exports.push(`export { TestBrowser } from './testBrowser';`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (api.appData.framework === 'react') {
|
||||||
|
if (api.config.ssr) {
|
||||||
|
exports.push(
|
||||||
|
`export { useServerInsertedHTML } from './core/serverInsertedHTMLContext';`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
exports.push(
|
||||||
|
`export const useServerInsertedHTML: Function = () => {};`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.push('// openinula');
|
||||||
|
exports.push(`export * from '${api.appData.openinula.path}';\n`);
|
||||||
|
|
||||||
|
// plugins
|
||||||
|
exports.push('// plugins');
|
||||||
|
const allPlugins = readdirSync(api.paths.absTmpPath).filter((file) =>
|
||||||
|
file.startsWith('plugin-'),
|
||||||
|
);
|
||||||
|
const plugins = allPlugins.filter((file) => {
|
||||||
|
if (
|
||||||
|
existsSync(join(api.paths.absTmpPath, file, 'index.ts')) ||
|
||||||
|
existsSync(join(api.paths.absTmpPath, file, 'index.tsx'))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
let file: string;
|
||||||
|
if (existsSync(join(api.paths.absTmpPath, plugin, 'index.ts'))) {
|
||||||
|
file = join(api.paths.absTmpPath, plugin, 'index.ts');
|
||||||
|
}
|
||||||
|
if (existsSync(join(api.paths.absTmpPath, plugin, 'index.tsx'))) {
|
||||||
|
file = join(api.paths.absTmpPath, plugin, 'index.tsx');
|
||||||
|
}
|
||||||
|
const pluginExports = await getExportsAndCheck({
|
||||||
|
path: file!,
|
||||||
|
exportMembers,
|
||||||
|
});
|
||||||
|
if (pluginExports.length) {
|
||||||
|
exports.push(
|
||||||
|
`export { ${pluginExports.join(', ')} } from '${winPath(
|
||||||
|
join(api.paths.absTmpPath, plugin),
|
||||||
|
)}';`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// plugins types.ts
|
||||||
|
exports.push('// plugins types.d.ts');
|
||||||
|
for (const plugin of allPlugins) {
|
||||||
|
const file = winPath(join(api.paths.absTmpPath, plugin, 'types.d.ts'));
|
||||||
|
if (existsSync(file)) {
|
||||||
|
// 带 .ts 后缀的声明文件 会导致声明失效
|
||||||
|
const noSuffixFile = file.replace(/\.ts$/, '');
|
||||||
|
exports.push(`export * from '${noSuffixFile}';`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// plugins runtimeConfig.d.ts
|
||||||
|
let pluginIndex = 0;
|
||||||
|
const beforeImport = [];
|
||||||
|
let runtimeConfigType =
|
||||||
|
'export type RuntimeConfig = IDefaultRuntimeConfig';
|
||||||
|
|
||||||
|
for (const plugin of allPlugins) {
|
||||||
|
const runtimeConfigFile = winPath(
|
||||||
|
join(api.paths.absTmpPath, plugin, RUNTIME_TYPE_FILE_NAME),
|
||||||
|
);
|
||||||
|
if (existsSync(runtimeConfigFile)) {
|
||||||
|
const noSuffixRuntimeConfigFile = runtimeConfigFile.replace(
|
||||||
|
/\.ts$/,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
beforeImport.push(
|
||||||
|
`import type { IRuntimeConfig as Plugin${pluginIndex} } from '${noSuffixRuntimeConfigFile}'`,
|
||||||
|
);
|
||||||
|
runtimeConfigType += ` & Plugin${pluginIndex}`;
|
||||||
|
pluginIndex += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api.writeTmpFile({
|
||||||
|
noPluginDir: true,
|
||||||
|
path: 'core/defineApp.ts',
|
||||||
|
tplPath: join(TEMPLATES_DIR, 'defineApp.tpl'),
|
||||||
|
context: {
|
||||||
|
beforeImport: beforeImport.join('\n'),
|
||||||
|
runtimeConfigType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// FIXME: if exported after plugins, circular dependency:
|
||||||
|
// `app.ts -> exports.ts -> plugin -> core/plugin.ts -> app.ts`
|
||||||
|
// we will get a `defineApp` of `undefined`
|
||||||
|
// https://github.com/umijs/umi/issues/9702
|
||||||
|
// https://github.com/umijs/umi/issues/10412
|
||||||
|
exports.unshift(
|
||||||
|
`export { defineApp } from './core/defineApp'`,
|
||||||
|
// https://javascript.plainenglish.io/leveraging-type-only-imports-and-exports-with-typescript-3-8-5c1be8bd17fb
|
||||||
|
`export type { RuntimeConfig } from './core/defineApp'`,
|
||||||
|
);
|
||||||
|
api.writeTmpFile({
|
||||||
|
noPluginDir: true,
|
||||||
|
path: 'exports.ts',
|
||||||
|
content: exports.join('\n'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
stage: 10000,
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
import index from './index';
|
||||||
|
|
||||||
|
test('normal', () => {
|
||||||
|
expect(index()).toEqual('@aluni/preset-inula');
|
||||||
|
});
|
|
@ -0,0 +1,131 @@
|
||||||
|
export * from '@aluni/types';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
return {
|
||||||
|
plugins: [
|
||||||
|
// registerMethods
|
||||||
|
require.resolve('@umijs/preset-umi/dist/registerMethods'),
|
||||||
|
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/404/404'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/appData/appData'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/appData/umiInfo'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/check/check'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/check/babel722'),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/codeSplitting/codeSplitting',
|
||||||
|
),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/configPlugins/configPlugins',
|
||||||
|
),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/crossorigin/crossorigin',
|
||||||
|
),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/depsOnDemand/depsOnDemand',
|
||||||
|
),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/devTool/devTool'),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/esbuildHelperChecker/esbuildHelperChecker',
|
||||||
|
),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/esmi/esmi'),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/exportStatic/exportStatic',
|
||||||
|
),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/favicons/favicons'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/helmet/helmet'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/icons/icons'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/mock/mock'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/mpa/mpa'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/okam/okam'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/overrides/overrides'),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/phantomDependency/phantomDependency',
|
||||||
|
),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/polyfill/polyfill'),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/polyfill/publicPathPolyfill',
|
||||||
|
),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/prepare/prepare'),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/routePrefetch/routePrefetch',
|
||||||
|
),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/terminal/terminal'),
|
||||||
|
|
||||||
|
// 1. generate tmp files
|
||||||
|
// @umijs/preset-umi/dist/features/tmpFiles/tmpFiles 使用 umi 形成循环依赖
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/features/tmpFiles/tmpFiles'),
|
||||||
|
require.resolve('./features/tmpFiles'),
|
||||||
|
|
||||||
|
// 2. `clientLoader` and `routeProps` depends on `tmpFiles` files
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/clientLoader/clientLoader',
|
||||||
|
),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/routeProps/routeProps'),
|
||||||
|
// 3. `ssr` needs to be run last
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/ssr/ssr'),
|
||||||
|
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/tmpFiles/configTypes'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/transform/transform'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/lowImport/lowImport'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/vite/vite'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/apiRoute/apiRoute'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/monorepo/redirect'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/test/test'),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/clickToComponent/clickToComponent',
|
||||||
|
),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/legacy/legacy'),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/classPropertiesLoose/classPropertiesLoose',
|
||||||
|
),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/webpack/webpack'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/swc/swc'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/features/ui/ui'),
|
||||||
|
require.resolve(
|
||||||
|
'@umijs/preset-umi/dist/features/hmrGuardian/hmrGuardian',
|
||||||
|
),
|
||||||
|
|
||||||
|
// commands
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/build'),
|
||||||
|
require.resolve('./commands/build'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/commands/config/config'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/dev/dev'),
|
||||||
|
require.resolve('./commands/dev'),
|
||||||
|
require.resolve('./commands/help'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/commands/lint'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/commands/setup'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/commands/deadcode'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/version'),
|
||||||
|
require.resolve('./commands/version'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/generators/page'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/generators/prettier'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/generators/tsconfig'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/generators/jest'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/generators/tailwindcss'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/generators/dva'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/generators/component'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/generators/mock'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/generators/cypress'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/generators/api'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/generators/precommit'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/commands/plugin'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/commands/verify-commit'),
|
||||||
|
require.resolve('@umijs/preset-umi/dist/commands/preview'),
|
||||||
|
// require.resolve('@umijs/preset-umi/dist/commands/mfsu/mfsu'),
|
||||||
|
// require.resolve('@umijs/plugin-run'),
|
||||||
|
require.resolve('@alita/plugin-azure'),
|
||||||
|
require.resolve('./config/inulaconfig'),
|
||||||
|
require.resolve('./features/iloading'),
|
||||||
|
|
||||||
|
// business
|
||||||
|
// 国际化插件要在前面,因为它提供了 api 供 antd 插件使用
|
||||||
|
require.resolve('../../plugin-intl/src'),
|
||||||
|
require.resolve('../../plugin-antd/src'),
|
||||||
|
require.resolve('../../plugin-antd-layout/src'),
|
||||||
|
require.resolve('../../plugin-request/src'),
|
||||||
|
require.resolve('../../plugin-x/src'),
|
||||||
|
require.resolve('../../plugin-openapi/src'),
|
||||||
|
|
||||||
|
].filter(Boolean),
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { resolve } from '@umijs/utils';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
export function resolveProjectDep(opts: {
|
||||||
|
pkg: any;
|
||||||
|
cwd: string;
|
||||||
|
dep: string;
|
||||||
|
}) {
|
||||||
|
if (
|
||||||
|
opts.pkg.dependencies?.[opts.dep] ||
|
||||||
|
opts.pkg.devDependencies?.[opts.dep]
|
||||||
|
) {
|
||||||
|
return dirname(
|
||||||
|
resolve.sync(`${opts.dep}/package.json`, {
|
||||||
|
basedir: opts.cwd,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const hookPropertyMap = new Map([
|
||||||
|
['inula', join(__dirname, './index.js')],
|
||||||
|
// why? 有些插件会引这个路径,但是 inula 没有依赖 umi 所以需要在这里改一下
|
||||||
|
['umi/plugin-utils', join(__dirname, '../plugin-utils.js')],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const mod = require('module');
|
||||||
|
const resolveFilename = mod._resolveFilename;
|
||||||
|
mod._resolveFilename = function (
|
||||||
|
request: string,
|
||||||
|
parent: any,
|
||||||
|
isMain: boolean,
|
||||||
|
options: any,
|
||||||
|
) {
|
||||||
|
const hookResolved = hookPropertyMap.get(request);
|
||||||
|
if (hookResolved) request = hookResolved;
|
||||||
|
return resolveFilename.call(mod, request, parent, isMain, options);
|
||||||
|
};
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { Service as CoreService } from '@umijs/core';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { dirname, isAbsolute, join } from 'path';
|
||||||
|
import * as process from 'process';
|
||||||
|
import { DEFAULT_CONFIG_FILES, FRAMEWORK_NAME } from './constants';
|
||||||
|
|
||||||
|
export class Service extends CoreService {
|
||||||
|
constructor(opts?: any) {
|
||||||
|
process.env.UMI_DIR = dirname(require.resolve('../package'));
|
||||||
|
|
||||||
|
let cwd = process.cwd();
|
||||||
|
require('./requireHook');
|
||||||
|
|
||||||
|
const appRoot = process.env.APP_ROOT;
|
||||||
|
|
||||||
|
if (appRoot) {
|
||||||
|
cwd = isAbsolute(appRoot) ? appRoot : join(cwd, appRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
super({
|
||||||
|
...opts,
|
||||||
|
env: process.env.NODE_ENV,
|
||||||
|
cwd,
|
||||||
|
defaultConfigFiles: DEFAULT_CONFIG_FILES,
|
||||||
|
frameworkName: FRAMEWORK_NAME,
|
||||||
|
presets: [
|
||||||
|
require.resolve('./plugins/preset-inula/src'),
|
||||||
|
require.resolve('@umijs/preset-blocks'),
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
require.resolve('./commands/format'),
|
||||||
|
require.resolve('./config/inulamain'),
|
||||||
|
existsSync(join(cwd, 'plugin.ts')) && join(cwd, 'plugin.ts'),
|
||||||
|
existsSync(join(cwd, 'plugin.js')) && join(cwd, 'plugin.js'),
|
||||||
|
].filter(Boolean),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async run2(opts: { name: string; args?: any }) {
|
||||||
|
let name = opts.name;
|
||||||
|
if (opts?.args.version || name === 'v') {
|
||||||
|
name = 'version';
|
||||||
|
} else if (opts?.args.help || !name || name === 'h') {
|
||||||
|
name = 'help';
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.run({ ...opts, name });
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"strict": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"target": "esnext",
|
||||||
|
"jsx": "react",
|
||||||
|
"paths": {
|
||||||
|
"inula-max": [
|
||||||
|
"./"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"**/node_modules",
|
||||||
|
"**/examples",
|
||||||
|
"**/dist",
|
||||||
|
"**/fixtures",
|
||||||
|
"**/*.test.ts"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue