mirror of
https://github.com/WeBankFinTech/fes.js.git
synced 2025-04-06 03:59:53 +08:00
feat: 去掉文件名大写
This commit is contained in:
parent
67903dab8b
commit
05d994e50d
@ -60,7 +60,8 @@ export function pathToObj({ path, cwd }) {
|
|||||||
if (pkgJSONPath) {
|
if (pkgJSONPath) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
pkg = require(pkgJSONPath);
|
pkg = require(pkgJSONPath);
|
||||||
isPkgPlugin = winPath(join(dirname(pkgJSONPath), pkg.main || 'index.js')) === winPath(path);
|
isPkgPlugin = winPath(join(dirname(pkgJSONPath), pkg.main || 'index.js'))
|
||||||
|
=== winPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
let id;
|
let id;
|
||||||
@ -107,7 +108,6 @@ export function resolvePlugins(opts) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function isValidPlugin(plugin) {
|
export function isValidPlugin(plugin) {
|
||||||
return plugin.id && plugin.key && plugin.apply;
|
return plugin.id && plugin.key && plugin.apply;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import {
|
import { existsSync } from 'fs';
|
||||||
existsSync
|
import { extname, join } from 'path';
|
||||||
} from 'fs';
|
|
||||||
import {
|
|
||||||
extname,
|
|
||||||
join
|
|
||||||
} from 'path';
|
|
||||||
import {
|
import {
|
||||||
chalk,
|
chalk,
|
||||||
chokidar,
|
chokidar,
|
||||||
@ -18,9 +13,7 @@ import {
|
|||||||
} from '@umijs/utils';
|
} from '@umijs/utils';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import joi from 'joi';
|
import joi from 'joi';
|
||||||
import {
|
import { ServiceStage } from '../service/enums';
|
||||||
ServiceStage
|
|
||||||
} from '../Service/enums';
|
|
||||||
import {
|
import {
|
||||||
getUserConfigWithKey,
|
getUserConfigWithKey,
|
||||||
updateUserConfigWithKey
|
updateUserConfigWithKey
|
||||||
@ -28,10 +21,7 @@ import {
|
|||||||
import isEqual from './utils/isEqual';
|
import isEqual from './utils/isEqual';
|
||||||
import mergeDefault from './utils/mergeDefault';
|
import mergeDefault from './utils/mergeDefault';
|
||||||
|
|
||||||
const CONFIG_FILES = [
|
const CONFIG_FILES = ['.fes.js', 'config/config.js'];
|
||||||
'.fes.js',
|
|
||||||
'config/config.js'
|
|
||||||
];
|
|
||||||
|
|
||||||
// TODO:
|
// TODO:
|
||||||
// 1. custom config file
|
// 1. custom config file
|
||||||
@ -57,10 +47,7 @@ export default class Config {
|
|||||||
|
|
||||||
// collect default config
|
// collect default config
|
||||||
const defaultConfig = pluginIds.reduce((memo, pluginId) => {
|
const defaultConfig = pluginIds.reduce((memo, pluginId) => {
|
||||||
const {
|
const { key, config = {} } = this.service.plugins[pluginId];
|
||||||
key,
|
|
||||||
config = {}
|
|
||||||
} = this.service.plugins[pluginId];
|
|
||||||
if ('default' in config) memo[key] = config.default;
|
if ('default' in config) memo[key] = config.default;
|
||||||
return memo;
|
return memo;
|
||||||
}, {});
|
}, {});
|
||||||
@ -68,26 +55,23 @@ export default class Config {
|
|||||||
return defaultConfig;
|
return defaultConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig({
|
getConfig({ defaultConfig }) {
|
||||||
defaultConfig
|
|
||||||
}) {
|
|
||||||
assert(
|
assert(
|
||||||
this.service.stage >= ServiceStage.pluginReady,
|
this.service.stage >= ServiceStage.pluginReady,
|
||||||
'Config.getConfig() failed, it should not be executed before plugin is ready.',
|
'Config.getConfig() failed, it should not be executed before plugin is ready.'
|
||||||
);
|
);
|
||||||
|
|
||||||
const userConfig = this.getUserConfig();
|
const userConfig = this.getUserConfig();
|
||||||
// 用于提示用户哪些 key 是未定义的
|
// 用于提示用户哪些 key 是未定义的
|
||||||
// TODO: 考虑不排除 false 的 key
|
// TODO: 考虑不排除 false 的 key
|
||||||
const userConfigKeys = Object.keys(userConfig).filter(key => userConfig[key] !== false);
|
const userConfigKeys = Object.keys(userConfig).filter(
|
||||||
|
key => userConfig[key] !== false
|
||||||
|
);
|
||||||
|
|
||||||
// get config
|
// get config
|
||||||
const pluginIds = Object.keys(this.service.plugins);
|
const pluginIds = Object.keys(this.service.plugins);
|
||||||
pluginIds.forEach((pluginId) => {
|
pluginIds.forEach((pluginId) => {
|
||||||
const {
|
const { key, config = {} } = this.service.plugins[pluginId];
|
||||||
key,
|
|
||||||
config = {}
|
|
||||||
} = this.service.plugins[pluginId];
|
|
||||||
// recognize as key if have schema config
|
// recognize as key if have schema config
|
||||||
if (!config.schema) return;
|
if (!config.schema) return;
|
||||||
|
|
||||||
@ -102,14 +86,12 @@ export default class Config {
|
|||||||
const schema = config.schema(joi);
|
const schema = config.schema(joi);
|
||||||
assert(
|
assert(
|
||||||
joi.isSchema(schema),
|
joi.isSchema(schema),
|
||||||
`schema return from plugin ${pluginId} is not valid schema.`,
|
`schema return from plugin ${pluginId} is not valid schema.`
|
||||||
);
|
);
|
||||||
const {
|
const { error } = schema.validate(value);
|
||||||
error
|
|
||||||
} = schema.validate(value);
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const e = new Error(
|
const e = new Error(
|
||||||
`Validate config "${key}" failed, ${error.message}`,
|
`Validate config "${key}" failed, ${error.message}`
|
||||||
);
|
);
|
||||||
e.stack = error.stack;
|
e.stack = error.stack;
|
||||||
throw e;
|
throw e;
|
||||||
@ -137,7 +119,9 @@ export default class Config {
|
|||||||
|
|
||||||
if (userConfigKeys.length) {
|
if (userConfigKeys.length) {
|
||||||
const keys = userConfigKeys.length > 1 ? 'keys' : 'key';
|
const keys = userConfigKeys.length > 1 ? 'keys' : 'key';
|
||||||
throw new Error(`Invalid config ${keys}: ${userConfigKeys.join(', ')}`);
|
throw new Error(
|
||||||
|
`Invalid config ${keys}: ${userConfigKeys.join(', ')}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return userConfig;
|
return userConfig;
|
||||||
@ -153,11 +137,11 @@ export default class Config {
|
|||||||
if (process.env.FES_ENV) {
|
if (process.env.FES_ENV) {
|
||||||
const envConfigFileName = this.addAffix(
|
const envConfigFileName = this.addAffix(
|
||||||
configFile,
|
configFile,
|
||||||
process.env.FES_ENV,
|
process.env.FES_ENV
|
||||||
);
|
);
|
||||||
const fileNameWithoutExt = envConfigFileName.replace(
|
const fileNameWithoutExt = envConfigFileName.replace(
|
||||||
extname(envConfigFileName),
|
extname(envConfigFileName),
|
||||||
'',
|
''
|
||||||
);
|
);
|
||||||
envConfigFile = getFile({
|
envConfigFile = getFile({
|
||||||
base: this.cwd,
|
base: this.cwd,
|
||||||
@ -166,7 +150,7 @@ export default class Config {
|
|||||||
}).filename;
|
}).filename;
|
||||||
if (!envConfigFile) {
|
if (!envConfigFile) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`get user config failed, ${envConfigFile} does not exist, but process.env.FES_ENV is set to ${process.env.FES_ENV}.`,
|
`get user config failed, ${envConfigFile} does not exist, but process.env.FES_ENV is set to ${process.env.FES_ENV}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -203,7 +187,7 @@ export default class Config {
|
|||||||
|
|
||||||
requireConfigs(configFiles) {
|
requireConfigs(configFiles) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
return configFiles.map(f => compatESModuleRequire(require(f)));
|
return configFiles.map((f) => compatESModuleRequire(require(f)));
|
||||||
}
|
}
|
||||||
|
|
||||||
mergeConfig(...configs) {
|
mergeConfig(...configs) {
|
||||||
@ -266,10 +250,7 @@ export default class Config {
|
|||||||
const pluginChanged = [];
|
const pluginChanged = [];
|
||||||
const valueChanged = [];
|
const valueChanged = [];
|
||||||
Object.keys(this.service.plugins).forEach((pluginId) => {
|
Object.keys(this.service.plugins).forEach((pluginId) => {
|
||||||
const {
|
const { key, config = {} } = this.service.plugins[pluginId];
|
||||||
key,
|
|
||||||
config = {}
|
|
||||||
} = this.service.plugins[pluginId];
|
|
||||||
// recognize as key if have schema config
|
// recognize as key if have schema config
|
||||||
if (!config.schema) return;
|
if (!config.schema) return;
|
||||||
if (!isEqual(newUserConfig[key], userConfig[key])) {
|
if (!isEqual(newUserConfig[key], userConfig[key])) {
|
||||||
@ -277,7 +258,10 @@ export default class Config {
|
|||||||
key,
|
key,
|
||||||
pluginId
|
pluginId
|
||||||
};
|
};
|
||||||
if (newUserConfig[key] === false || userConfig[key] === false) {
|
if (
|
||||||
|
newUserConfig[key] === false
|
||||||
|
|| userConfig[key] === false
|
||||||
|
) {
|
||||||
pluginChanged.push(changed);
|
pluginChanged.push(changed);
|
||||||
} else {
|
} else {
|
||||||
valueChanged.push(changed);
|
valueChanged.push(changed);
|
17
packages/fes-core/src/config/utils/configUtils.js
Normal file
17
packages/fes-core/src/config/utils/configUtils.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { lodash } from '@umijs/utils';
|
||||||
|
import set from 'set-value';
|
||||||
|
|
||||||
|
export function updateUserConfigWithKey({
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
userConfig
|
||||||
|
}) {
|
||||||
|
set(userConfig, key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserConfigWithKey({
|
||||||
|
key,
|
||||||
|
userConfig
|
||||||
|
}) {
|
||||||
|
return lodash.get(userConfig, key);
|
||||||
|
}
|
16
packages/fes-core/src/config/utils/isEqual.js
Normal file
16
packages/fes-core/src/config/utils/isEqual.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { lodash } from '@umijs/utils';
|
||||||
|
|
||||||
|
function funcToStr(obj) {
|
||||||
|
if (typeof obj === 'function') return obj.toString();
|
||||||
|
if (lodash.isPlainObject(obj)) {
|
||||||
|
return Object.keys(obj).reduce((memo, key) => {
|
||||||
|
memo[key] = funcToStr(obj[key]);
|
||||||
|
return memo;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (a, b) {
|
||||||
|
return lodash.isEqual(funcToStr(a), funcToStr(b));
|
||||||
|
}
|
9
packages/fes-core/src/config/utils/mergeDefault.js
Normal file
9
packages/fes-core/src/config/utils/mergeDefault.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { deepmerge, lodash } from '@umijs/utils';
|
||||||
|
|
||||||
|
|
||||||
|
export default ({ defaultConfig, config }) => {
|
||||||
|
if (lodash.isPlainObject(defaultConfig) && lodash.isPlainObject(config)) {
|
||||||
|
return deepmerge(defaultConfig, config);
|
||||||
|
}
|
||||||
|
return typeof config !== 'undefined' ? config : defaultConfig;
|
||||||
|
};
|
@ -1,11 +1,11 @@
|
|||||||
|
|
||||||
|
|
||||||
import Config from './Config/Config';
|
import Config from './config';
|
||||||
import Service from './Service/Service';
|
import Service from './service';
|
||||||
import PluginAPI from './Service/PluginAPI';
|
import PluginAPI from './service/pluginAPI';
|
||||||
import Logger from './logger/logger';
|
import Logger from './logger/logger';
|
||||||
import { PluginType } from './Service/enums';
|
import { PluginType } from './service/enums';
|
||||||
import { isPlugin } from './Service/utils/pluginUtils';
|
import { isPlugin } from './service/utils/pluginUtils';
|
||||||
|
|
||||||
export * from './route';
|
export * from './route';
|
||||||
|
|
||||||
|
32
packages/fes-core/src/service/enums.js
Normal file
32
packages/fes-core/src/service/enums.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
export const PluginType = {
|
||||||
|
preset: 'preset',
|
||||||
|
plugin: 'plugin'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ServiceStage = {
|
||||||
|
uninitialized: 0,
|
||||||
|
constructor: 1,
|
||||||
|
init: 2,
|
||||||
|
initPlugins: 3,
|
||||||
|
initHooks: 4,
|
||||||
|
pluginReady: 5,
|
||||||
|
getConfig: 6,
|
||||||
|
getPaths: 7,
|
||||||
|
run: 8
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ConfigChangeType = {
|
||||||
|
reload: 'reload',
|
||||||
|
regenerateTmpFiles: 'regenerateTmpFiles'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ApplyPluginsType = {
|
||||||
|
add: 'add',
|
||||||
|
modify: 'modify',
|
||||||
|
event: 'event'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EnableBy = {
|
||||||
|
register: 'register',
|
||||||
|
config: 'config'
|
||||||
|
};
|
37
packages/fes-core/src/service/getPaths.js
Normal file
37
packages/fes-core/src/service/getPaths.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { join } from 'path';
|
||||||
|
import { existsSync, statSync } from 'fs';
|
||||||
|
import { lodash, winPath } from '@umijs/utils';
|
||||||
|
|
||||||
|
function isDirectoryAndExist(path) {
|
||||||
|
return existsSync(path) && statSync(path).isDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWithWinPath(obj) {
|
||||||
|
return lodash.mapValues(obj, value => winPath(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function getServicePaths({
|
||||||
|
cwd,
|
||||||
|
config,
|
||||||
|
env
|
||||||
|
}) {
|
||||||
|
let absSrcPath = cwd;
|
||||||
|
if (isDirectoryAndExist(join(cwd, 'src'))) {
|
||||||
|
absSrcPath = join(cwd, 'src');
|
||||||
|
}
|
||||||
|
const absPagesPath = config.singular
|
||||||
|
? join(absSrcPath, 'page')
|
||||||
|
: join(absSrcPath, 'pages');
|
||||||
|
|
||||||
|
const tmpDir = ['.fes', env !== 'development' && env]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('-');
|
||||||
|
return normalizeWithWinPath({
|
||||||
|
cwd,
|
||||||
|
absNodeModulesPath: join(cwd, 'node_modules'),
|
||||||
|
absOutputPath: join(cwd, config.outputPath || './dist'),
|
||||||
|
absSrcPath,
|
||||||
|
absPagesPath,
|
||||||
|
absTmpPath: join(absSrcPath, tmpDir)
|
||||||
|
});
|
||||||
|
}
|
@ -1,33 +1,21 @@
|
|||||||
import {
|
import { join } from 'path';
|
||||||
join
|
import { EventEmitter } from 'events';
|
||||||
} from 'path';
|
|
||||||
import {
|
|
||||||
EventEmitter
|
|
||||||
} from 'events';
|
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import {
|
import { AsyncSeriesWaterfallHook } from 'tapable';
|
||||||
AsyncSeriesWaterfallHook
|
import { existsSync } from 'fs';
|
||||||
} from 'tapable';
|
|
||||||
import {
|
|
||||||
existsSync
|
|
||||||
} from 'fs';
|
|
||||||
import { BabelRegister } from '@umijs/utils';
|
import { BabelRegister } from '@umijs/utils';
|
||||||
import {
|
import { resolvePlugins } from './utils/pluginUtils';
|
||||||
resolvePlugins
|
|
||||||
} from './utils/pluginUtils';
|
|
||||||
import loadDotEnv from './utils/loadDotEnv';
|
import loadDotEnv from './utils/loadDotEnv';
|
||||||
import isPromise from './utils/isPromise';
|
import isPromise from './utils/isPromise';
|
||||||
import PluginAPI from './PluginAPI';
|
import PluginAPI from './pluginAPI';
|
||||||
import {
|
import {
|
||||||
ApplyPluginsType,
|
ApplyPluginsType,
|
||||||
ConfigChangeType,
|
ConfigChangeType,
|
||||||
EnableBy,
|
EnableBy,
|
||||||
ServiceStage
|
ServiceStage
|
||||||
} from './enums';
|
} from './enums';
|
||||||
import Config from '../Config/Config';
|
import Config from '../config';
|
||||||
import {
|
import { getUserConfigWithKey } from '../config/utils/configUtils';
|
||||||
getUserConfigWithKey
|
|
||||||
} from '../Config/utils/configUtils';
|
|
||||||
import getPaths from './getPaths';
|
import getPaths from './getPaths';
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
@ -137,7 +125,7 @@ export default class Service extends EventEmitter {
|
|||||||
resolvePackage() {
|
resolvePackage() {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
return require(join(this.cwd, 'package.json'));
|
return require(join(this.cwd, "package.json"));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@ -161,9 +149,7 @@ export default class Service extends EventEmitter {
|
|||||||
Object.keys(this.hooksByPluginId).forEach((id) => {
|
Object.keys(this.hooksByPluginId).forEach((id) => {
|
||||||
const hooks = this.hooksByPluginId[id];
|
const hooks = this.hooksByPluginId[id];
|
||||||
hooks.forEach((hook) => {
|
hooks.forEach((hook) => {
|
||||||
const {
|
const { key } = hook;
|
||||||
key
|
|
||||||
} = hook;
|
|
||||||
hook.pluginId = id;
|
hook.pluginId = id;
|
||||||
this.hooks[key] = (this.hooks[key] || []).concat(hook);
|
this.hooks[key] = (this.hooks[key] || []).concat(hook);
|
||||||
});
|
});
|
||||||
@ -188,11 +174,11 @@ export default class Service extends EventEmitter {
|
|||||||
if (this.config.outputPath) {
|
if (this.config.outputPath) {
|
||||||
this.paths.absOutputPath = join(this.cwd, this.config.outputPath);
|
this.paths.absOutputPath = join(this.cwd, this.config.outputPath);
|
||||||
}
|
}
|
||||||
const paths = (await this.applyPlugins({
|
const paths = await this.applyPlugins({
|
||||||
key: 'modifyPaths',
|
key: 'modifyPaths',
|
||||||
type: ApplyPluginsType.modify,
|
type: ApplyPluginsType.modify,
|
||||||
initialValue: this.paths
|
initialValue: this.paths
|
||||||
}));
|
});
|
||||||
Object.keys(paths).forEach((key) => {
|
Object.keys(paths).forEach((key) => {
|
||||||
this.paths[key] = paths[key];
|
this.paths[key] = paths[key];
|
||||||
});
|
});
|
||||||
@ -283,11 +269,7 @@ export default class Service extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initPlugin(plugin) {
|
async initPlugin(plugin) {
|
||||||
const {
|
const { id, key, apply } = plugin;
|
||||||
id,
|
|
||||||
key,
|
|
||||||
apply
|
|
||||||
} = plugin;
|
|
||||||
|
|
||||||
const api = this.getPluginAPI({
|
const api = this.getPluginAPI({
|
||||||
id,
|
id,
|
||||||
@ -318,10 +300,7 @@ export default class Service extends EventEmitter {
|
|||||||
// api.skipPlugins() 的插件
|
// api.skipPlugins() 的插件
|
||||||
if (this.skipPluginIds.has(pluginId)) return false;
|
if (this.skipPluginIds.has(pluginId)) return false;
|
||||||
|
|
||||||
const {
|
const { key, enableBy } = this.plugins[pluginId];
|
||||||
key,
|
|
||||||
enableBy
|
|
||||||
} = this.plugins[pluginId];
|
|
||||||
|
|
||||||
// 手动设置为 false
|
// 手动设置为 false
|
||||||
if (this.userConfig[key] === false) return false;
|
if (this.userConfig[key] === false) return false;
|
||||||
@ -354,16 +333,17 @@ export default class Service extends EventEmitter {
|
|||||||
if ('initialValue' in opts) {
|
if ('initialValue' in opts) {
|
||||||
assert(
|
assert(
|
||||||
Array.isArray(opts.initialValue),
|
Array.isArray(opts.initialValue),
|
||||||
'applyPlugins failed, opts.initialValue must be Array if opts.type is add.',
|
'applyPlugins failed, opts.initialValue must be Array if opts.type is add.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const tAdd = new AsyncSeriesWaterfallHook(['memo']);
|
const tAdd = new AsyncSeriesWaterfallHook(["memo"]);
|
||||||
for (const hook of hooks) {
|
for (const hook of hooks) {
|
||||||
if (!this.isPluginEnable(hook.pluginId)) {
|
if (!this.isPluginEnable(hook.pluginId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
tAdd.tapPromise({
|
tAdd.tapPromise(
|
||||||
|
{
|
||||||
name: hook.pluginId,
|
name: hook.pluginId,
|
||||||
stage: hook.stage || 0,
|
stage: hook.stage || 0,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -372,33 +352,37 @@ export default class Service extends EventEmitter {
|
|||||||
async (memo) => {
|
async (memo) => {
|
||||||
const items = await hook.fn(opts.args);
|
const items = await hook.fn(opts.args);
|
||||||
return memo.concat(items);
|
return memo.concat(items);
|
||||||
},);
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return tAdd.promise(opts.initialValue || []);
|
return tAdd.promise(opts.initialValue || []);
|
||||||
case ApplyPluginsType.modify:
|
case ApplyPluginsType.modify:
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const tModify = new AsyncSeriesWaterfallHook(['memo']);
|
const tModify = new AsyncSeriesWaterfallHook(["memo"]);
|
||||||
for (const hook of hooks) {
|
for (const hook of hooks) {
|
||||||
if (!this.isPluginEnable(hook.pluginId)) {
|
if (!this.isPluginEnable(hook.pluginId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
tModify.tapPromise({
|
tModify.tapPromise(
|
||||||
|
{
|
||||||
name: hook.pluginId,
|
name: hook.pluginId,
|
||||||
stage: hook.stage || 0,
|
stage: hook.stage || 0,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
before: hook.before
|
before: hook.before
|
||||||
},
|
},
|
||||||
async memo => hook.fn(memo, opts.args),);
|
async memo => hook.fn(memo, opts.args)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return tModify.promise(opts.initialValue);
|
return tModify.promise(opts.initialValue);
|
||||||
case ApplyPluginsType.event:
|
case ApplyPluginsType.event:
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const tEvent = new AsyncSeriesWaterfallHook(['_']);
|
const tEvent = new AsyncSeriesWaterfallHook(["_"]);
|
||||||
for (const hook of hooks) {
|
for (const hook of hooks) {
|
||||||
if (!this.isPluginEnable(hook.pluginId)) {
|
if (!this.isPluginEnable(hook.pluginId)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
tEvent.tapPromise({
|
tEvent.tapPromise(
|
||||||
|
{
|
||||||
name: hook.pluginId,
|
name: hook.pluginId,
|
||||||
stage: hook.stage || 0,
|
stage: hook.stage || 0,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -406,20 +390,18 @@ export default class Service extends EventEmitter {
|
|||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
await hook.fn(opts.args);
|
await hook.fn(opts.args);
|
||||||
},);
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return tEvent.promise();
|
return tEvent.promise();
|
||||||
default:
|
default:
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`applyPlugin failed, type is not defined or is not matched, got ${opts.type}.`,
|
`applyPlugin failed, type is not defined or is not matched, got ${opts.type}.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async run({
|
async run({ name, args = {} }) {
|
||||||
name,
|
|
||||||
args = {}
|
|
||||||
}) {
|
|
||||||
args._ = args._ || [];
|
args._ = args._ || [];
|
||||||
// shift the command itself
|
// shift the command itself
|
||||||
if (args._[0] === name) args._.shift();
|
if (args._[0] === name) args._.shift();
|
||||||
@ -442,10 +424,7 @@ export default class Service extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async runCommand({
|
async runCommand({ name, args = {} }) {
|
||||||
name,
|
|
||||||
args = {}
|
|
||||||
}) {
|
|
||||||
assert(this.stage >= ServiceStage.init, 'service is not initialized.');
|
assert(this.stage >= ServiceStage.init, 'service is not initialized.');
|
||||||
|
|
||||||
args._ = args._ || [];
|
args._ = args._ || [];
|
||||||
@ -457,9 +436,7 @@ export default class Service extends EventEmitter {
|
|||||||
: this.commands[name];
|
: this.commands[name];
|
||||||
assert(command, `run command failed, command ${name} does not exists.`);
|
assert(command, `run command failed, command ${name} does not exists.`);
|
||||||
|
|
||||||
const {
|
const { fn } = command;
|
||||||
fn
|
|
||||||
} = command;
|
|
||||||
return fn({
|
return fn({
|
||||||
args
|
args
|
||||||
});
|
});
|
134
packages/fes-core/src/service/pluginAPI.js
Normal file
134
packages/fes-core/src/service/pluginAPI.js
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import * as utils from '@umijs/utils';
|
||||||
|
import { isValidPlugin, pathToObj } from './utils/pluginUtils';
|
||||||
|
import { EnableBy, PluginType, ServiceStage } from './enums';
|
||||||
|
import Logger from '../logger/logger';
|
||||||
|
// TODO
|
||||||
|
// 标准化 logger
|
||||||
|
export default class PluginAPI {
|
||||||
|
constructor(opts) {
|
||||||
|
this.id = opts.id;
|
||||||
|
this.key = opts.key;
|
||||||
|
this.service = opts.service;
|
||||||
|
this.utils = utils;
|
||||||
|
this.logger = new Logger(`fes:plugin:${this.id || this.key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: reversed keys
|
||||||
|
describe({
|
||||||
|
id,
|
||||||
|
key,
|
||||||
|
config,
|
||||||
|
enableBy
|
||||||
|
} = {}) {
|
||||||
|
const { plugins } = this.service;
|
||||||
|
// this.id and this.key is generated automatically
|
||||||
|
// so we need to diff first
|
||||||
|
if (id && this.id !== id) {
|
||||||
|
if (plugins[id]) {
|
||||||
|
const name = plugins[id].isPreset ? 'preset' : 'plugin';
|
||||||
|
throw new Error(
|
||||||
|
`api.describe() failed, ${name} ${id} is already registered by ${plugins[id].path}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
plugins[id] = plugins[this.id];
|
||||||
|
plugins[id].id = id;
|
||||||
|
delete plugins[this.id];
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
if (key && this.key !== key) {
|
||||||
|
this.key = key;
|
||||||
|
plugins[this.id].key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
plugins[this.id].config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins[this.id].enableBy = enableBy || EnableBy.register;
|
||||||
|
}
|
||||||
|
|
||||||
|
register(hook) {
|
||||||
|
assert(
|
||||||
|
hook.key && typeof hook.key === 'string',
|
||||||
|
`api.register() failed, hook.key must supplied and should be string, but got ${hook.key}.`,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
hook.fn && typeof hook.fn === 'function',
|
||||||
|
`api.register() failed, hook.fn must supplied and should be function, but got ${hook.fn}.`,
|
||||||
|
);
|
||||||
|
this.service.hooksByPluginId[this.id] = (
|
||||||
|
this.service.hooksByPluginId[this.id] || []
|
||||||
|
).concat(hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerCommand(command) {
|
||||||
|
const { name, alias } = command;
|
||||||
|
assert(
|
||||||
|
!this.service.commands[name],
|
||||||
|
`api.registerCommand() failed, the command ${name} is exists.`,
|
||||||
|
);
|
||||||
|
this.service.commands[name] = command;
|
||||||
|
if (alias) {
|
||||||
|
this.service.commands[alias] = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在 preset 初始化阶段放后面,在插件注册阶段放前面
|
||||||
|
registerPlugins(plugins) {
|
||||||
|
assert(
|
||||||
|
this.service.stage === ServiceStage.initPresets
|
||||||
|
|| this.service.stage === ServiceStage.initPlugins,
|
||||||
|
'api.registerPlugins() failed, it should only be used in registering stage.',
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
Array.isArray(plugins),
|
||||||
|
'api.registerPlugins() failed, plugins must be Array.',
|
||||||
|
);
|
||||||
|
const extraPlugins = plugins.map(plugin => (isValidPlugin(plugin)
|
||||||
|
? (plugin)
|
||||||
|
: pathToObj({
|
||||||
|
type: PluginType.plugin,
|
||||||
|
path: plugin,
|
||||||
|
cwd: this.service.cwd
|
||||||
|
})));
|
||||||
|
if (this.service.stage === ServiceStage.initPresets) {
|
||||||
|
this.service._extraPlugins.push(...extraPlugins);
|
||||||
|
} else {
|
||||||
|
this.service._extraPlugins.splice(0, 0, ...extraPlugins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerMethod({
|
||||||
|
name,
|
||||||
|
fn,
|
||||||
|
exitsError = true
|
||||||
|
}) {
|
||||||
|
if (this.service.pluginMethods[name]) {
|
||||||
|
if (exitsError) {
|
||||||
|
throw new Error(
|
||||||
|
`api.registerMethod() failed, method ${name} is already exist.`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.service.pluginMethods[name] = fn
|
||||||
|
// 这里不能用 arrow function,this 需指向执行此方法的 PluginAPI
|
||||||
|
// 否则 pluginId 会不会,导致不能正确 skip plugin
|
||||||
|
|| function (hookFn) {
|
||||||
|
const hook = {
|
||||||
|
key: name,
|
||||||
|
...(utils.lodash.isPlainObject(hookFn) ? hookFn : { fn: hookFn })
|
||||||
|
};
|
||||||
|
// @ts-ignore
|
||||||
|
this.register(hook);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
skipPlugins(pluginIds) {
|
||||||
|
pluginIds.forEach((pluginId) => {
|
||||||
|
this.service.skipPluginIds.add(pluginId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
7
packages/fes-core/src/service/utils/isPromise.js
Normal file
7
packages/fes-core/src/service/utils/isPromise.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default function isPromise(obj) {
|
||||||
|
return (
|
||||||
|
!!obj
|
||||||
|
&& (typeof obj === 'object' || typeof obj === 'function')
|
||||||
|
&& typeof obj.then === 'function'
|
||||||
|
);
|
||||||
|
}
|
18
packages/fes-core/src/service/utils/loadDotEnv.js
Normal file
18
packages/fes-core/src/service/utils/loadDotEnv.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { readFileSync, existsSync } from 'fs';
|
||||||
|
import { parse } from 'dotenv';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dotenv wrapper
|
||||||
|
* @param envPath string
|
||||||
|
*/
|
||||||
|
export default function loadDotEnv(envPath) {
|
||||||
|
if (existsSync(envPath)) {
|
||||||
|
const parsed = parse(readFileSync(envPath, 'utf-8')) || {};
|
||||||
|
Object.keys(parsed).forEach((key) => {
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (!process.env.hasOwnProperty(key)) {
|
||||||
|
process.env[key] = parsed[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
113
packages/fes-core/src/service/utils/pluginUtils.js
Normal file
113
packages/fes-core/src/service/utils/pluginUtils.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import {
|
||||||
|
dirname, join, basename, relative, extname
|
||||||
|
} from 'path';
|
||||||
|
import {
|
||||||
|
compatESModuleRequire,
|
||||||
|
resolve,
|
||||||
|
winPath,
|
||||||
|
pkgUp,
|
||||||
|
lodash
|
||||||
|
} from '@umijs/utils';
|
||||||
|
|
||||||
|
const RE = {
|
||||||
|
plugin: /^(@webank\/)?fes-plugin-/
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isPlugin(name) {
|
||||||
|
const hasScope = name.charAt(0) === '@';
|
||||||
|
const re = RE.plugin;
|
||||||
|
if (hasScope) {
|
||||||
|
return re.test(name.split('/')[1]) || re.test(name);
|
||||||
|
}
|
||||||
|
return re.test(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlugins(opts) {
|
||||||
|
return [
|
||||||
|
// dependencies
|
||||||
|
...opts.plugins,
|
||||||
|
...Object.keys(opts.pkg.devDependencies || {})
|
||||||
|
.concat(Object.keys(opts.pkg.dependencies || {}))
|
||||||
|
.filter(isPlugin.bind(null)),
|
||||||
|
...opts.userConfigPlugins
|
||||||
|
].map(path => resolve.sync(path, {
|
||||||
|
basedir: opts.cwd,
|
||||||
|
extensions: ['.js', '.ts']
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// e.g.
|
||||||
|
// initial-state -> initialState
|
||||||
|
// webpack.css-loader -> webpack.cssLoader
|
||||||
|
function nameToKey(name) {
|
||||||
|
return name
|
||||||
|
.split('.')
|
||||||
|
.map(part => lodash.camelCase(part))
|
||||||
|
.join('.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function pkgNameToKey(pkgName) {
|
||||||
|
if (pkgName.charAt(0) === '@' && !pkgName.startsWith('@webank/')) {
|
||||||
|
pkgName = pkgName.split('/')[1];
|
||||||
|
}
|
||||||
|
return nameToKey(pkgName.replace(RE.plugin, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pathToObj({ path, cwd }) {
|
||||||
|
let pkg = null;
|
||||||
|
let isPkgPlugin = false;
|
||||||
|
const pkgJSONPath = pkgUp.sync({ cwd: path });
|
||||||
|
if (pkgJSONPath) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
pkg = require(pkgJSONPath);
|
||||||
|
isPkgPlugin = winPath(join(dirname(pkgJSONPath), pkg.main || 'index.js'))
|
||||||
|
=== winPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let id;
|
||||||
|
if (isPkgPlugin) {
|
||||||
|
id = pkg.name;
|
||||||
|
} else if (winPath(path).startsWith(winPath(cwd))) {
|
||||||
|
id = `./${winPath(relative(cwd, path))}`;
|
||||||
|
} else if (pkgJSONPath) {
|
||||||
|
id = winPath(join(pkg.name, relative(dirname(pkgJSONPath), path)));
|
||||||
|
} else {
|
||||||
|
id = winPath(path);
|
||||||
|
}
|
||||||
|
id = id.replace('@webank/fes-plugin-built-in/lib/plugins', '@@');
|
||||||
|
id = id.replace(/\.js$/, '');
|
||||||
|
|
||||||
|
const key = isPkgPlugin
|
||||||
|
? pkgNameToKey(pkg.name)
|
||||||
|
: nameToKey(basename(path, extname(path)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
key,
|
||||||
|
path: winPath(path),
|
||||||
|
apply() {
|
||||||
|
// use function to delay require
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const ret = require(path);
|
||||||
|
// use the default member for es modules
|
||||||
|
return compatESModuleRequire(ret);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Register ${path} failed, since ${e.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultConfig: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePlugins(opts) {
|
||||||
|
const plugins = getPlugins(opts);
|
||||||
|
return plugins.map(path => pathToObj({
|
||||||
|
path,
|
||||||
|
cwd: opts.cwd
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidPlugin(plugin) {
|
||||||
|
return plugin.id && plugin.key && plugin.apply;
|
||||||
|
}
|
@ -10,4 +10,4 @@ export {
|
|||||||
createRouter
|
createRouter
|
||||||
} from 'vue-router';
|
} from 'vue-router';
|
||||||
|
|
||||||
export { default as Plugin, ApplyPluginsType } from './Plugin/Plugin';
|
export { default as Plugin, ApplyPluginsType } from './plugin';
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// fes.config.js 只负责管理和 cli 相关的配置
|
// fes.config.js 只负责管理 cli 相关的配置
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// fes.config.js 只负责管理和 cli 相关的配置
|
// fes.config.js 只负责管理 cli 相关的配置
|
||||||
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
24
yarn.lock
24
yarn.lock
@ -1404,14 +1404,6 @@
|
|||||||
pirates "^4.0.0"
|
pirates "^4.0.0"
|
||||||
source-map-support "^0.5.9"
|
source-map-support "^0.5.9"
|
||||||
|
|
||||||
"@babel/runtime-corejs3@^7.11.2":
|
|
||||||
version "7.12.1"
|
|
||||||
resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.1.tgz#51b9092befbeeed938335a109dbe0df51451e9dc"
|
|
||||||
integrity sha512-umhPIcMrlBZ2aTWlWjUseW9LjQKxi1dpFlQS8DzsxB//5K+u6GLTC/JliPKHsd5kJVPIU6X/Hy0YvWOYPcMxBw==
|
|
||||||
dependencies:
|
|
||||||
core-js-pure "^3.0.0"
|
|
||||||
regenerator-runtime "^0.13.4"
|
|
||||||
|
|
||||||
"@babel/runtime@7.10.4":
|
"@babel/runtime@7.10.4":
|
||||||
version "7.10.4"
|
version "7.10.4"
|
||||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99"
|
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.4.tgz#a6724f1a6b8d2f6ea5236dbfe58c7d7ea9c5eb99"
|
||||||
@ -4930,7 +4922,7 @@ chardet@^0.7.0:
|
|||||||
resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||||
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
|
||||||
|
|
||||||
cheerio@1.0.0-rc.3, cheerio@^1.0.0-rc.3:
|
cheerio@1.0.0-rc.3:
|
||||||
version "1.0.0-rc.3"
|
version "1.0.0-rc.3"
|
||||||
resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
|
resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
|
||||||
integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
|
integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
|
||||||
@ -5619,11 +5611,6 @@ core-js-compat@^3.1.1, core-js-compat@^3.6.2:
|
|||||||
browserslist "^4.8.5"
|
browserslist "^4.8.5"
|
||||||
semver "7.0.0"
|
semver "7.0.0"
|
||||||
|
|
||||||
core-js-pure@^3.0.0:
|
|
||||||
version "3.6.5"
|
|
||||||
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
|
|
||||||
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
|
|
||||||
|
|
||||||
core-js@3.6.5, core-js@^3.0.0, core-js@^3.6.1:
|
core-js@3.6.5, core-js@^3.0.0, core-js@^3.6.1:
|
||||||
version "3.6.5"
|
version "3.6.5"
|
||||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
resolved "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a"
|
||||||
@ -5742,15 +5729,6 @@ crypto-random-string@^1.0.0:
|
|||||||
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
|
resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
|
||||||
integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
|
integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
|
||||||
|
|
||||||
csp-html-webpack-plugin@^4.0.0:
|
|
||||||
version "4.0.0"
|
|
||||||
resolved "https://registry.npmjs.org/csp-html-webpack-plugin/-/csp-html-webpack-plugin-4.0.0.tgz#1ed2cd0dee23186587f9642084715b163345b59b"
|
|
||||||
integrity sha512-1YqQefNG0SrZisysThlly2bgs4Ab/W91xOM17S8wd+6vTo3E0OdL+y4IAR0MKpthRluNGzFB3QhPqdOhkXAExg==
|
|
||||||
dependencies:
|
|
||||||
cheerio "^1.0.0-rc.3"
|
|
||||||
lodash "^4.17.15"
|
|
||||||
memory-fs "^0.5.0"
|
|
||||||
|
|
||||||
css-blank-pseudo@^0.1.4:
|
css-blank-pseudo@^0.1.4:
|
||||||
version "0.1.4"
|
version "0.1.4"
|
||||||
resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
|
resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz#dfdefd3254bf8a82027993674ccf35483bfcb3c5"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user