Merge pull request '新增inula max 内容' (#8) from xiaohuoni/inula:master into master

This commit is contained in:
openinula 2024-10-21 14:06:44 +08:00
commit 68ad2d3e9c
78 changed files with 5360 additions and 0 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ pnpm-lock.yaml
build build
/packages/inula-router/connectRouter /packages/inula-router/connectRouter
/packages/inula-router/router /packages/inula-router/router
.inula-max

12
packages/max/.fatherrc.ts Normal file
View File

@ -0,0 +1,12 @@
import { defineConfig } from 'father';
export default defineConfig({
cjs: {
output: 'dist',
ignores: ['src/client/**'],
},
esm: {
input: 'src/client',
output: 'client/client',
},
});

21
packages/max/.gitignore vendored Normal file
View File

@ -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

58
packages/max/README.md Normal file
View File

@ -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) 许可证开源。

14
packages/max/bin/inula.js Executable file
View File

@ -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);
});

35
packages/max/client/client/plugin.d.ts vendored Normal file
View File

@ -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

View File

@ -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

7
packages/max/client/client/utils.d.ts vendored Normal file
View File

@ -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

View File

@ -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"}

View File

@ -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';
}

View File

@ -0,0 +1,5 @@
import { defineConfig } from 'inula-max';
export default defineConfig({
title: 'boilerplate',
});

View File

@ -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:.."
}
}

View File

@ -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;

View File

@ -0,0 +1,13 @@
import { createStore } from 'inula';
export default createStore({
id: 'hello12',
actions: {
changeName: (state) => {
state.title = 'openinula';
},
},
state: {
title: 'inulajs',
},
});

View File

@ -0,0 +1,13 @@
import { createStore } from 'inula';
export default createStore({
id: 'hello',
actions: {
changeName: (state) => {
state.title = 'openinula';
},
},
state: {
title: 'inulajs',
},
});

View File

@ -0,0 +1,10 @@
{
"extends": "./src/.inula-max/tsconfig.json",
"compilerOptions":{
"paths": {
"inula-max": [
"../../"
]
},
}
}

1
packages/max/demo/typings.d.ts vendored Normal file
View File

@ -0,0 +1 @@
import "inula/typings";

7
packages/max/eslint.js Normal file
View File

@ -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');

View File

10
packages/max/index.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
// @ts-ignore
export * from '@@/exports';
export type {
IApi,
webpack,
IRoute,
UmiApiRequest,
UmiApiResponse,
} from '@aluni/types';
export * from './dist';

55
packages/max/package.json Normal file
View File

@ -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"
}
}

1
packages/max/plugin-utils.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export * from './dist/pluginUtils';

View File

@ -0,0 +1 @@
module.exports = require('./dist/pluginUtils');

13
packages/max/prettier.js Normal file
View File

@ -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,
};

64
packages/max/src/cli.ts Normal file
View File

@ -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);
}
}

View File

@ -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();
});

View File

@ -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

View File

@ -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';
}

View File

@ -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`);
}
},
});
};

View File

@ -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');
});
};

View File

@ -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'];

View File

@ -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;
}

View File

@ -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;

33
packages/max/src/node.ts Normal file
View File

@ -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';
}

View File

@ -0,0 +1,2 @@
// 只导出了 utils 的方法,如果有用到 bundler-utils 再增加
export * from '@umijs/utils';

View File

@ -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,
},
];
});
};

View File

@ -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,
),
);
}

View File

@ -0,0 +1,3 @@
import { join } from 'path';
export const TEMPLATES_DIR = join(__dirname, '../templates');

View File

@ -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(() => {});
};

View File

@ -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,
),
);
}

View File

@ -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: '語言',
},
};

View File

@ -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 完成');
},
});
};

View File

@ -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
);
}

View File

@ -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,
),
);
}

View File

@ -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);

View File

@ -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);
},
});
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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')}
}`;
}
}

View File

@ -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;
});
}

View File

@ -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,
}),
);
}
}

View File

@ -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);
};

View File

@ -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,
),
);
}

View File

@ -0,0 +1,3 @@
import { join } from 'path';
export const TEMPLATES_DIR = join(__dirname, '../templates');

View File

@ -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;
});
};

View File

@ -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,
),
);
}

View File

@ -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;
}

View File

@ -0,0 +1 @@
export const INULA_KEYS = ['create', 'use', 'clear'];

View File

@ -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: [],
});
}

View File

@ -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')}
`;
}
}

View File

@ -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

View File

@ -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
},
});
};

View File

@ -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;
});
};

View File

@ -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');
}
};

View File

@ -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;
},
});
};

View File

@ -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;
});
};

View File

@ -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

View File

@ -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'),
),
});
};

View File

@ -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,
});
};

View File

@ -0,0 +1,5 @@
import index from './index';
test('normal', () => {
expect(index()).toEqual('@aluni/preset-inula');
});

View File

@ -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),
};
};

View File

@ -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,
}),
);
}
}

View File

@ -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);
};

View File

@ -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 });
}
}

View File

@ -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"
]
}