Merge pull request '增加inula max 框架' (#1) from feat-inula-max into master
This commit is contained in:
commit
70f7bb3f51
|
@ -8,3 +8,4 @@ pnpm-lock.yaml
|
|||
build
|
||||
/packages/inula-router/connectRouter
|
||||
/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