diff --git a/.gitignore b/.gitignore index 8636e699..8863b9b0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ pnpm-lock.yaml build /packages/inula-router/connectRouter /packages/inula-router/router +.inula-max diff --git a/packages/max/.fatherrc.ts b/packages/max/.fatherrc.ts new file mode 100644 index 00000000..11585725 --- /dev/null +++ b/packages/max/.fatherrc.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'father'; + +export default defineConfig({ + cjs: { + output: 'dist', + ignores: ['src/client/**'], + }, + esm: { + input: 'src/client', + output: 'client/client', + }, +}); diff --git a/packages/max/.gitignore b/packages/max/.gitignore new file mode 100644 index 00000000..94a296e5 --- /dev/null +++ b/packages/max/.gitignore @@ -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 diff --git a/packages/max/README.md b/packages/max/README.md new file mode 100644 index 00000000..7cb3b6ee --- /dev/null +++ b/packages/max/README.md @@ -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) 许可证开源。 diff --git a/packages/max/bin/inula.js b/packages/max/bin/inula.js new file mode 100755 index 00000000..737030ff --- /dev/null +++ b/packages/max/bin/inula.js @@ -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); + }); diff --git a/packages/max/client/client/plugin.d.ts b/packages/max/client/client/plugin.d.ts new file mode 100644 index 00000000..9496f4ff --- /dev/null +++ b/packages/max/client/client/plugin.d.ts @@ -0,0 +1,35 @@ +export declare enum ApplyPluginsType { + compose = "compose", + modify = "modify", + event = "event" +} +interface IPlugin { + path?: string; + apply: Record; +} +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 \ No newline at end of file diff --git a/packages/max/client/client/plugin.d.ts.map b/packages/max/client/client/plugin.d.ts.map new file mode 100644 index 00000000..dbad871e --- /dev/null +++ b/packages/max/client/client/plugin.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../src/client/plugin.ts"],"names":[],"mappings":"AAEA,oBAAY,gBAAgB;IAC1B,OAAO,YAAY;IACnB,MAAM,WAAW;IACjB,KAAK,UAAU;CAChB;AAED,UAAU,OAAO;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B;AAED,qBAAa,aAAa;IACxB,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAC9B,KAAK,EAAE;QACL,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB,CAAM;gBACK,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE;IAIzC,QAAQ,CAAC,MAAM,EAAE,OAAO;IAaxB,QAAQ,CAAC,UAAU,EAAE,MAAM;IAqB3B,YAAY,CAAC,EACX,GAAG,EACH,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,KAAK,GACN,EAAE;QACD,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,gBAAgB,CAAC;QACvB,YAAY,CAAC,EAAE,GAAG,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB;IAuFD,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,EAAE,CAAC;QAAC,OAAO,EAAE,OAAO,EAAE,CAAA;KAAE;CAShE"} \ No newline at end of file diff --git a/packages/max/client/client/plugin.js b/packages/max/client/client/plugin.js new file mode 100644 index 00000000..52b44b05 --- /dev/null +++ b/packages/max/client/client/plugin.js @@ -0,0 +1,228 @@ +function _regeneratorRuntime() { "use strict"; /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ _regeneratorRuntime = function _regeneratorRuntime() { return e; }; var t, e = {}, r = Object.prototype, n = r.hasOwnProperty, o = Object.defineProperty || function (t, e, r) { t[e] = r.value; }, i = "function" == typeof Symbol ? Symbol : {}, a = i.iterator || "@@iterator", c = i.asyncIterator || "@@asyncIterator", u = i.toStringTag || "@@toStringTag"; function define(t, e, r) { return Object.defineProperty(t, e, { value: r, enumerable: !0, configurable: !0, writable: !0 }), t[e]; } try { define({}, ""); } catch (t) { define = function define(t, e, r) { return t[e] = r; }; } function wrap(t, e, r, n) { var i = e && e.prototype instanceof Generator ? e : Generator, a = Object.create(i.prototype), c = new Context(n || []); return o(a, "_invoke", { value: makeInvokeMethod(t, r, c) }), a; } function tryCatch(t, e, r) { try { return { type: "normal", arg: t.call(e, r) }; } catch (t) { return { type: "throw", arg: t }; } } e.wrap = wrap; var h = "suspendedStart", l = "suspendedYield", f = "executing", s = "completed", y = {}; function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} var p = {}; define(p, a, function () { return this; }); var d = Object.getPrototypeOf, v = d && d(d(values([]))); v && v !== r && n.call(v, a) && (p = v); var g = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(p); function defineIteratorMethods(t) { ["next", "throw", "return"].forEach(function (e) { define(t, e, function (t) { return this._invoke(e, t); }); }); } function AsyncIterator(t, e) { function invoke(r, o, i, a) { var c = tryCatch(t[r], t, o); if ("throw" !== c.type) { var u = c.arg, h = u.value; return h && "object" == _typeof(h) && n.call(h, "__await") ? e.resolve(h.__await).then(function (t) { invoke("next", t, i, a); }, function (t) { invoke("throw", t, i, a); }) : e.resolve(h).then(function (t) { u.value = t, i(u); }, function (t) { return invoke("throw", t, i, a); }); } a(c.arg); } var r; o(this, "_invoke", { value: function value(t, n) { function callInvokeWithMethodAndArg() { return new e(function (e, r) { invoke(t, n, e, r); }); } return r = r ? r.then(callInvokeWithMethodAndArg, callInvokeWithMethodAndArg) : callInvokeWithMethodAndArg(); } }); } function makeInvokeMethod(e, r, n) { var o = h; return function (i, a) { if (o === f) throw new Error("Generator is already running"); if (o === s) { if ("throw" === i) throw a; return { value: t, done: !0 }; } for (n.method = i, n.arg = a;;) { var c = n.delegate; if (c) { var u = maybeInvokeDelegate(c, n); if (u) { if (u === y) continue; return u; } } if ("next" === n.method) n.sent = n._sent = n.arg;else if ("throw" === n.method) { if (o === h) throw o = s, n.arg; n.dispatchException(n.arg); } else "return" === n.method && n.abrupt("return", n.arg); o = f; var p = tryCatch(e, r, n); if ("normal" === p.type) { if (o = n.done ? s : l, p.arg === y) continue; return { value: p.arg, done: n.done }; } "throw" === p.type && (o = s, n.method = "throw", n.arg = p.arg); } }; } function maybeInvokeDelegate(e, r) { var n = r.method, o = e.iterator[n]; if (o === t) return r.delegate = null, "throw" === n && e.iterator.return && (r.method = "return", r.arg = t, maybeInvokeDelegate(e, r), "throw" === r.method) || "return" !== n && (r.method = "throw", r.arg = new TypeError("The iterator does not provide a '" + n + "' method")), y; var i = tryCatch(o, e.iterator, r.arg); if ("throw" === i.type) return r.method = "throw", r.arg = i.arg, r.delegate = null, y; var a = i.arg; return a ? a.done ? (r[e.resultName] = a.value, r.next = e.nextLoc, "return" !== r.method && (r.method = "next", r.arg = t), r.delegate = null, y) : a : (r.method = "throw", r.arg = new TypeError("iterator result is not an object"), r.delegate = null, y); } function pushTryEntry(t) { var e = { tryLoc: t[0] }; 1 in t && (e.catchLoc = t[1]), 2 in t && (e.finallyLoc = t[2], e.afterLoc = t[3]), this.tryEntries.push(e); } function resetTryEntry(t) { var e = t.completion || {}; e.type = "normal", delete e.arg, t.completion = e; } function Context(t) { this.tryEntries = [{ tryLoc: "root" }], t.forEach(pushTryEntry, this), this.reset(!0); } function values(e) { if (e || "" === e) { var r = e[a]; if (r) return r.call(e); if ("function" == typeof e.next) return e; if (!isNaN(e.length)) { var o = -1, i = function next() { for (; ++o < e.length;) if (n.call(e, o)) return next.value = e[o], next.done = !1, next; return next.value = t, next.done = !0, next; }; return i.next = i; } } throw new TypeError(_typeof(e) + " is not iterable"); } return GeneratorFunction.prototype = GeneratorFunctionPrototype, o(g, "constructor", { value: GeneratorFunctionPrototype, configurable: !0 }), o(GeneratorFunctionPrototype, "constructor", { value: GeneratorFunction, configurable: !0 }), GeneratorFunction.displayName = define(GeneratorFunctionPrototype, u, "GeneratorFunction"), e.isGeneratorFunction = function (t) { var e = "function" == typeof t && t.constructor; return !!e && (e === GeneratorFunction || "GeneratorFunction" === (e.displayName || e.name)); }, e.mark = function (t) { return Object.setPrototypeOf ? Object.setPrototypeOf(t, GeneratorFunctionPrototype) : (t.__proto__ = GeneratorFunctionPrototype, define(t, u, "GeneratorFunction")), t.prototype = Object.create(g), t; }, e.awrap = function (t) { return { __await: t }; }, defineIteratorMethods(AsyncIterator.prototype), define(AsyncIterator.prototype, c, function () { return this; }), e.AsyncIterator = AsyncIterator, e.async = function (t, r, n, o, i) { void 0 === i && (i = Promise); var a = new AsyncIterator(wrap(t, r, n, o), i); return e.isGeneratorFunction(r) ? a : a.next().then(function (t) { return t.done ? t.value : a.next(); }); }, defineIteratorMethods(g), define(g, u, "Generator"), define(g, a, function () { return this; }), define(g, "toString", function () { return "[object Generator]"; }), e.keys = function (t) { var e = Object(t), r = []; for (var n in e) r.push(n); return r.reverse(), function next() { for (; r.length;) { var t = r.pop(); if (t in e) return next.value = t, next.done = !1, next; } return next.done = !0, next; }; }, e.values = values, Context.prototype = { constructor: Context, reset: function reset(e) { if (this.prev = 0, this.next = 0, this.sent = this._sent = t, this.done = !1, this.delegate = null, this.method = "next", this.arg = t, this.tryEntries.forEach(resetTryEntry), !e) for (var r in this) "t" === r.charAt(0) && n.call(this, r) && !isNaN(+r.slice(1)) && (this[r] = t); }, stop: function stop() { this.done = !0; var t = this.tryEntries[0].completion; if ("throw" === t.type) throw t.arg; return this.rval; }, dispatchException: function dispatchException(e) { if (this.done) throw e; var r = this; function handle(n, o) { return a.type = "throw", a.arg = e, r.next = n, o && (r.method = "next", r.arg = t), !!o; } for (var o = this.tryEntries.length - 1; o >= 0; --o) { var i = this.tryEntries[o], a = i.completion; if ("root" === i.tryLoc) return handle("end"); if (i.tryLoc <= this.prev) { var c = n.call(i, "catchLoc"), u = n.call(i, "finallyLoc"); if (c && u) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } else if (c) { if (this.prev < i.catchLoc) return handle(i.catchLoc, !0); } else { if (!u) throw new Error("try statement without catch or finally"); if (this.prev < i.finallyLoc) return handle(i.finallyLoc); } } } }, abrupt: function abrupt(t, e) { for (var r = this.tryEntries.length - 1; r >= 0; --r) { var o = this.tryEntries[r]; if (o.tryLoc <= this.prev && n.call(o, "finallyLoc") && this.prev < o.finallyLoc) { var i = o; break; } } i && ("break" === t || "continue" === t) && i.tryLoc <= e && e <= i.finallyLoc && (i = null); var a = i ? i.completion : {}; return a.type = t, a.arg = e, i ? (this.method = "next", this.next = i.finallyLoc, y) : this.complete(a); }, complete: function complete(t, e) { if ("throw" === t.type) throw t.arg; return "break" === t.type || "continue" === t.type ? this.next = t.arg : "return" === t.type ? (this.rval = this.arg = t.arg, this.method = "return", this.next = "end") : "normal" === t.type && e && (this.next = e), y; }, finish: function finish(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.finallyLoc === t) return this.complete(r.completion, r.afterLoc), resetTryEntry(r), y; } }, catch: function _catch(t) { for (var e = this.tryEntries.length - 1; e >= 0; --e) { var r = this.tryEntries[e]; if (r.tryLoc === t) { var n = r.completion; if ("throw" === n.type) { var o = n.arg; resetTryEntry(r); } return o; } } throw new Error("illegal catch attempt"); }, delegateYield: function delegateYield(e, r, n) { return this.delegate = { iterator: values(e), resultName: r, nextLoc: n }, "next" === this.method && (this.arg = t), y; } }, e; } +function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } +function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } +function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } +function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } +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); } +function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } +function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableRest(); } +function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } +function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } +function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } +function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } } +function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } +function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } +function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); } +function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } +import { assert, compose, isPromiseLike } from "./utils"; +export var ApplyPluginsType = /*#__PURE__*/function (ApplyPluginsType) { + ApplyPluginsType["compose"] = "compose"; + ApplyPluginsType["modify"] = "modify"; + ApplyPluginsType["event"] = "event"; + return ApplyPluginsType; +}({}); +export var PluginManager = /*#__PURE__*/function () { + function PluginManager(opts) { + _classCallCheck(this, PluginManager); + _defineProperty(this, "opts", void 0); + _defineProperty(this, "hooks", {}); + this.opts = opts; + } + _createClass(PluginManager, [{ + key: "register", + value: function register(plugin) { + var _this = this; + assert(plugin.apply, "plugin register failed, apply must supplied"); + Object.keys(plugin.apply).forEach(function (key) { + assert(_this.opts.validKeys.indexOf(key) > -1, "register failed, invalid key ".concat(key, " ").concat(plugin.path ? "from plugin ".concat(plugin.path) : '', ".")); + _this.hooks[key] = (_this.hooks[key] || []).concat(plugin.apply[key]); + }); + } + }, { + key: "getHooks", + value: function getHooks(keyWithDot) { + var _keyWithDot$split = keyWithDot.split('.'), + _keyWithDot$split2 = _toArray(_keyWithDot$split), + key = _keyWithDot$split2[0], + memberKeys = _keyWithDot$split2.slice(1); + var hooks = this.hooks[key] || []; + if (memberKeys.length) { + hooks = hooks.map(function (hook) { + try { + var ret = hook; + var _iterator = _createForOfIteratorHelper(memberKeys), + _step; + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var memberKey = _step.value; + ret = ret[memberKey]; + } + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); + } + return ret; + } catch (e) { + return null; + } + }).filter(Boolean); + } + return hooks; + } + }, { + key: "applyPlugins", + value: function applyPlugins(_ref) { + var key = _ref.key, + type = _ref.type, + initialValue = _ref.initialValue, + args = _ref.args, + async = _ref.async; + var 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( /*#__PURE__*/function () { + var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee(memo, hook) { + var ret; + return _regeneratorRuntime().wrap(function _callee$(_context) { + while (1) switch (_context.prev = _context.next) { + case 0: + assert(typeof hook === 'function' || _typeof(hook) === 'object' || isPromiseLike(hook), "applyPlugins failed, all hooks for key ".concat(key, " must be function, plain object or Promise.")); + if (!isPromiseLike(memo)) { + _context.next = 5; + break; + } + _context.next = 4; + return memo; + case 4: + memo = _context.sent; + case 5: + if (!(typeof hook === 'function')) { + _context.next = 16; + break; + } + ret = hook(memo, args); + if (!isPromiseLike(ret)) { + _context.next = 13; + break; + } + _context.next = 10; + return ret; + case 10: + return _context.abrupt("return", _context.sent); + case 13: + return _context.abrupt("return", ret); + case 14: + _context.next = 21; + break; + case 16: + if (!isPromiseLike(hook)) { + _context.next = 20; + break; + } + _context.next = 19; + return hook; + case 19: + hook = _context.sent; + case 20: + return _context.abrupt("return", _objectSpread(_objectSpread({}, memo), hook)); + case 21: + case "end": + return _context.stop(); + } + }, _callee); + })); + return function (_x, _x2) { + return _ref2.apply(this, arguments); + }; + }(), isPromiseLike(initialValue) ? initialValue : Promise.resolve(initialValue)); + } else { + return hooks.reduce(function (memo, hook) { + assert(typeof hook === 'function' || _typeof(hook) === 'object', "applyPlugins failed, all hooks for key ".concat(key, " must be function or plain object.")); + if (typeof hook === 'function') { + return hook(memo, args); + } else { + // TODO: deepmerge? + return _objectSpread(_objectSpread({}, memo), hook); + } + }, initialValue); + } + case ApplyPluginsType.event: + return _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2() { + var _iterator2, _step2, hook, ret; + return _regeneratorRuntime().wrap(function _callee2$(_context2) { + while (1) switch (_context2.prev = _context2.next) { + case 0: + _iterator2 = _createForOfIteratorHelper(hooks); + _context2.prev = 1; + _iterator2.s(); + case 3: + if ((_step2 = _iterator2.n()).done) { + _context2.next = 12; + break; + } + hook = _step2.value; + assert(typeof hook === 'function', "applyPlugins failed, all hooks for key ".concat(key, " must be function.")); + ret = hook(args); + if (!(async && isPromiseLike(ret))) { + _context2.next = 10; + break; + } + _context2.next = 10; + return ret; + case 10: + _context2.next = 3; + break; + case 12: + _context2.next = 17; + break; + case 14: + _context2.prev = 14; + _context2.t0 = _context2["catch"](1); + _iterator2.e(_context2.t0); + case 17: + _context2.prev = 17; + _iterator2.f(); + return _context2.finish(17); + case 20: + case "end": + return _context2.stop(); + } + }, _callee2, null, [[1, 14, 17, 20]]); + }))(); + case ApplyPluginsType.compose: + return function () { + return compose({ + fns: hooks.concat(initialValue), + args: args + })(); + }; + } + } + }], [{ + key: "create", + value: function create(opts) { + var pluginManager = new PluginManager({ + validKeys: opts.validKeys + }); + opts.plugins.forEach(function (plugin) { + pluginManager.register(plugin); + }); + return pluginManager; + } + }]); + return PluginManager; +}(); + +// plugins meta info (in tmp file) +// hooks api: usePlugin \ No newline at end of file diff --git a/packages/max/client/client/utils.d.ts b/packages/max/client/client/utils.d.ts new file mode 100644 index 00000000..4279b83d --- /dev/null +++ b/packages/max/client/client/utils.d.ts @@ -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 \ No newline at end of file diff --git a/packages/max/client/client/utils.d.ts.map b/packages/max/client/client/utils.d.ts.map new file mode 100644 index 00000000..c1ea581c --- /dev/null +++ b/packages/max/client/client/utils.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/client/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,QAErD;AAED,wBAAgB,OAAO,CAAC,EACtB,GAAG,EACH,IAAI,GACL,EAAE;IACD,GAAG,EAAE,CAAC,QAAQ,GAAG,GAAG,CAAC,EAAE,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,OAMA;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,WAErC"} \ No newline at end of file diff --git a/packages/max/client/client/utils.js b/packages/max/client/client/utils.js new file mode 100644 index 00000000..adc196bb --- /dev/null +++ b/packages/max/client/client/utils.js @@ -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'; +} \ No newline at end of file diff --git a/packages/max/demo/.inularc.ts b/packages/max/demo/.inularc.ts new file mode 100644 index 00000000..278f1f44 --- /dev/null +++ b/packages/max/demo/.inularc.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'inula-max'; + +export default defineConfig({ + title: 'boilerplate', +}); diff --git a/packages/max/demo/package.json b/packages/max/demo/package.json new file mode 100644 index 00000000..2625d32e --- /dev/null +++ b/packages/max/demo/package.json @@ -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:.." + } +} diff --git a/packages/max/demo/src/pages/index.tsx b/packages/max/demo/src/pages/index.tsx new file mode 100644 index 00000000..363af540 --- /dev/null +++ b/packages/max/demo/src/pages/index.tsx @@ -0,0 +1,19 @@ +import { useStore } from 'inula'; + +const Page = () => { + const store = useStore('hello'); + return ( +
+ hello {store.title} + +
+ ); +}; + +export default Page; diff --git a/packages/max/demo/src/pages/store.ts b/packages/max/demo/src/pages/store.ts new file mode 100644 index 00000000..b5d1fa28 --- /dev/null +++ b/packages/max/demo/src/pages/store.ts @@ -0,0 +1,13 @@ +import { createStore } from 'inula'; + +export default createStore({ + id: 'hello12', + actions: { + changeName: (state) => { + state.title = 'openinula'; + }, + }, + state: { + title: 'inulajs', + }, +}); diff --git a/packages/max/demo/src/stores/hello.ts b/packages/max/demo/src/stores/hello.ts new file mode 100644 index 00000000..972d748e --- /dev/null +++ b/packages/max/demo/src/stores/hello.ts @@ -0,0 +1,13 @@ +import { createStore } from 'inula'; + +export default createStore({ + id: 'hello', + actions: { + changeName: (state) => { + state.title = 'openinula'; + }, + }, + state: { + title: 'inulajs', + }, +}); diff --git a/packages/max/demo/tsconfig.json b/packages/max/demo/tsconfig.json new file mode 100644 index 00000000..4e608b29 --- /dev/null +++ b/packages/max/demo/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./src/.inula-max/tsconfig.json", + "compilerOptions":{ + "paths": { + "inula-max": [ + "../../" + ] + }, + } +} diff --git a/packages/max/demo/typings.d.ts b/packages/max/demo/typings.d.ts new file mode 100644 index 00000000..610c3e7a --- /dev/null +++ b/packages/max/demo/typings.d.ts @@ -0,0 +1 @@ +import "inula/typings"; diff --git a/packages/max/eslint.js b/packages/max/eslint.js new file mode 100644 index 00000000..c0eb233d --- /dev/null +++ b/packages/max/eslint.js @@ -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'); diff --git a/packages/max/fixtures/generate/.gitkeep b/packages/max/fixtures/generate/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/max/index.d.ts b/packages/max/index.d.ts new file mode 100644 index 00000000..24d99ca5 --- /dev/null +++ b/packages/max/index.d.ts @@ -0,0 +1,10 @@ +// @ts-ignore +export * from '@@/exports'; +export type { + IApi, + webpack, + IRoute, + UmiApiRequest, + UmiApiResponse, +} from '@aluni/types'; +export * from './dist'; diff --git a/packages/max/package.json b/packages/max/package.json new file mode 100644 index 00000000..0c95a4b1 --- /dev/null +++ b/packages/max/package.json @@ -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 (https://github.com/xiaohuoni)" + ], + "devDependencies": { + "father": "^4.5.0" + } +} diff --git a/packages/max/plugin-utils.d.ts b/packages/max/plugin-utils.d.ts new file mode 100644 index 00000000..665445cf --- /dev/null +++ b/packages/max/plugin-utils.d.ts @@ -0,0 +1 @@ +export * from './dist/pluginUtils'; diff --git a/packages/max/plugin-utils.js b/packages/max/plugin-utils.js new file mode 100644 index 00000000..f1aae50a --- /dev/null +++ b/packages/max/plugin-utils.js @@ -0,0 +1 @@ +module.exports = require('./dist/pluginUtils'); diff --git a/packages/max/prettier.js b/packages/max/prettier.js new file mode 100644 index 00000000..43e3aa0b --- /dev/null +++ b/packages/max/prettier.js @@ -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, +}; diff --git a/packages/max/src/cli.ts b/packages/max/src/cli.ts new file mode 100644 index 00000000..e28f2d4f --- /dev/null +++ b/packages/max/src/cli.ts @@ -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); + } +} diff --git a/packages/max/src/client/plugin.test.ts b/packages/max/src/client/plugin.test.ts new file mode 100644 index 00000000..d32d19a4 --- /dev/null +++ b/packages/max/src/client/plugin.test.ts @@ -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(); +}); diff --git a/packages/max/src/client/plugin.ts b/packages/max/src/client/plugin.ts new file mode 100644 index 00000000..5984bccc --- /dev/null +++ b/packages/max/src/client/plugin.ts @@ -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; +} + +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 | 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 diff --git a/packages/max/src/client/utils.ts b/packages/max/src/client/utils.ts new file mode 100644 index 00000000..6c83f2be --- /dev/null +++ b/packages/max/src/client/utils.ts @@ -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'; +} diff --git a/packages/max/src/commands/format.ts b/packages/max/src/commands/format.ts new file mode 100644 index 00000000..0ca7b554 --- /dev/null +++ b/packages/max/src/commands/format.ts @@ -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`); + } + }, + }); +}; diff --git a/packages/max/src/config/inulamain.ts b/packages/max/src/config/inulamain.ts new file mode 100644 index 00000000..f50f846d --- /dev/null +++ b/packages/max/src/config/inulamain.ts @@ -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'); + }); +}; diff --git a/packages/max/src/constants.ts b/packages/max/src/constants.ts new file mode 100644 index 00000000..df787d1d --- /dev/null +++ b/packages/max/src/constants.ts @@ -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']; diff --git a/packages/max/src/defineConfig.ts b/packages/max/src/defineConfig.ts new file mode 100644 index 00000000..e1bf4098 --- /dev/null +++ b/packages/max/src/defineConfig.ts @@ -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; +} diff --git a/packages/max/src/index.ts b/packages/max/src/index.ts new file mode 100644 index 00000000..683ea84b --- /dev/null +++ b/packages/max/src/index.ts @@ -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; diff --git a/packages/max/src/node.ts b/packages/max/src/node.ts new file mode 100644 index 00000000..1cd12973 --- /dev/null +++ b/packages/max/src/node.ts @@ -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'; +} diff --git a/packages/max/src/pluginUtils.ts b/packages/max/src/pluginUtils.ts new file mode 100644 index 00000000..ad3893a6 --- /dev/null +++ b/packages/max/src/pluginUtils.ts @@ -0,0 +1,2 @@ +// 只导出了 utils 的方法,如果有用到 bundler-utils 再增加 +export * from '@umijs/utils'; diff --git a/packages/max/src/plugins/plugin-antd-layout/src/index.ts b/packages/max/src/plugins/plugin-antd-layout/src/index.ts new file mode 100644 index 00000000..a9d69d1a --- /dev/null +++ b/packages/max/src/plugins/plugin-antd-layout/src/index.ts @@ -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 | undefined>( + settingDrawer, + ); + const outlet = useOutlet(); + const location = useLocation(); + const { pathname } = location; + if (typeof document === 'undefined') { + return
; + } + return ( +
+ + + + + {outlet} + + { + if (typeof window === 'undefined') return e; + return document.getElementById('inula-pro-layout'); + }} + settings={settings} + onSettingChange={(changeSetting) => { + setSetting(changeSetting); + }} + disableUrlParams={false} + /> + + + +
+ ); + }; + `, + 'utf-8', + ); + }); + api.addLayouts(() => { + return [ + { + id: 'ant-design-pro-layout', + file: api.config.proLayout.tmpPath || defaultTmpPath, + }, + ]; + }); +}; diff --git a/packages/max/src/plugins/plugin-antd-layout/src/withTmpPath.ts b/packages/max/src/plugins/plugin-antd-layout/src/withTmpPath.ts new file mode 100644 index 00000000..b69f6f48 --- /dev/null +++ b/packages/max/src/plugins/plugin-antd-layout/src/withTmpPath.ts @@ -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, + ), + ); +} diff --git a/packages/max/src/plugins/plugin-antd/src/constants.ts b/packages/max/src/plugins/plugin-antd/src/constants.ts new file mode 100644 index 00000000..770487af --- /dev/null +++ b/packages/max/src/plugins/plugin-antd/src/constants.ts @@ -0,0 +1,3 @@ +import { join } from 'path'; + +export const TEMPLATES_DIR = join(__dirname, '../templates'); diff --git a/packages/max/src/plugins/plugin-antd/src/index.ts b/packages/max/src/plugins/plugin-antd/src/index.ts new file mode 100644 index 00000000..2726d75f --- /dev/null +++ b/packages/max/src/plugins/plugin-antd/src/index.ts @@ -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(() => {}); +}; diff --git a/packages/max/src/plugins/plugin-antd/src/withTmpPath.ts b/packages/max/src/plugins/plugin-antd/src/withTmpPath.ts new file mode 100644 index 00000000..b69f6f48 --- /dev/null +++ b/packages/max/src/plugins/plugin-antd/src/withTmpPath.ts @@ -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, + ), + ); +} diff --git a/packages/max/src/plugins/plugin-intl/src/constants.ts b/packages/max/src/plugins/plugin-intl/src/constants.ts new file mode 100644 index 00000000..ec95e277 --- /dev/null +++ b/packages/max/src/plugins/plugin-intl/src/constants.ts @@ -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: '語言', + }, +}; diff --git a/packages/max/src/plugins/plugin-intl/src/index.ts b/packages/max/src/plugins/plugin-intl/src/index.ts new file mode 100644 index 00000000..311ec820 --- /dev/null +++ b/packages/max/src/plugins/plugin-intl/src/index.ts @@ -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 => { + 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 {props.children}; +} +`, + 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 完成'); + }, + }); +}; diff --git a/packages/max/src/plugins/plugin-intl/src/localeUtils.ts b/packages/max/src/plugins/plugin-intl/src/localeUtils.ts new file mode 100644 index 00000000..9d9a6d07 --- /dev/null +++ b/packages/max/src/plugins/plugin-intl/src/localeUtils.ts @@ -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 => { + 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 + ); +} diff --git a/packages/max/src/plugins/plugin-intl/src/withTmpPath.ts b/packages/max/src/plugins/plugin-intl/src/withTmpPath.ts new file mode 100644 index 00000000..b69f6f48 --- /dev/null +++ b/packages/max/src/plugins/plugin-intl/src/withTmpPath.ts @@ -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, + ), + ); +} diff --git a/packages/max/src/plugins/plugin-intl/templates/localeExports.tpl b/packages/max/src/plugins/plugin-intl/templates/localeExports.tpl new file mode 100644 index 00000000..bb4cc2f1 --- /dev/null +++ b/packages/max/src/plugins/plugin-intl/templates/localeExports.tpl @@ -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, + prefix = '', +) => { + return Object.keys(nestedMessages).reduce( + (messages: Record, 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); diff --git a/packages/max/src/plugins/plugin-openapi/src/index.ts b/packages/max/src/plugins/plugin-openapi/src/index.ts new file mode 100644 index 00000000..674814f0 --- /dev/null +++ b/packages/max/src/plugins/plugin-openapi/src/index.ts @@ -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 ( +
+ +
+
+ ); + }; + 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); + }, + }); +}; diff --git a/packages/max/src/plugins/plugin-openapi/src/utils/astUtils.ts b/packages/max/src/plugins/plugin-openapi/src/utils/astUtils.ts new file mode 100644 index 00000000..7daa5185 --- /dev/null +++ b/packages/max/src/plugins/plugin-openapi/src/utils/astUtils.ts @@ -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; +} diff --git a/packages/max/src/plugins/plugin-openapi/src/utils/getFile/getFile.ts b/packages/max/src/plugins/plugin-openapi/src/utils/getFile/getFile.ts new file mode 100644 index 00000000..8811202f --- /dev/null +++ b/packages/max/src/plugins/plugin-openapi/src/utils/getFile/getFile.ts @@ -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 = { + 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; +} diff --git a/packages/max/src/plugins/plugin-openapi/src/utils/modelUtils.ts b/packages/max/src/plugins/plugin-openapi/src/utils/modelUtils.ts new file mode 100644 index 00000000..82d67e83 --- /dev/null +++ b/packages/max/src/plugins/plugin-openapi/src/utils/modelUtils.ts @@ -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 = 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, + ) => { + 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')} +}`; + } +} diff --git a/packages/max/src/plugins/plugin-openapi/src/utils/resetMainPath/resetMainPath.ts b/packages/max/src/plugins/plugin-openapi/src/utils/resetMainPath/resetMainPath.ts new file mode 100644 index 00000000..4cbb6c60 --- /dev/null +++ b/packages/max/src/plugins/plugin-openapi/src/utils/resetMainPath/resetMainPath.ts @@ -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; + }); +} diff --git a/packages/max/src/plugins/plugin-openapi/src/utils/resolveProjectDep.ts b/packages/max/src/plugins/plugin-openapi/src/utils/resolveProjectDep.ts new file mode 100644 index 00000000..55b20af1 --- /dev/null +++ b/packages/max/src/plugins/plugin-openapi/src/utils/resolveProjectDep.ts @@ -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, + }), + ); + } +} diff --git a/packages/max/src/plugins/plugin-openapi/src/utils/symlink.ts b/packages/max/src/plugins/plugin-openapi/src/utils/symlink.ts new file mode 100644 index 00000000..493e6f22 --- /dev/null +++ b/packages/max/src/plugins/plugin-openapi/src/utils/symlink.ts @@ -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); +}; diff --git a/packages/max/src/plugins/plugin-openapi/src/utils/withTmpPath.ts b/packages/max/src/plugins/plugin-openapi/src/utils/withTmpPath.ts new file mode 100644 index 00000000..b69f6f48 --- /dev/null +++ b/packages/max/src/plugins/plugin-openapi/src/utils/withTmpPath.ts @@ -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, + ), + ); +} diff --git a/packages/max/src/plugins/plugin-request/src/constants.ts b/packages/max/src/plugins/plugin-request/src/constants.ts new file mode 100644 index 00000000..770487af --- /dev/null +++ b/packages/max/src/plugins/plugin-request/src/constants.ts @@ -0,0 +1,3 @@ +import { join } from 'path'; + +export const TEMPLATES_DIR = join(__dirname, '../templates'); diff --git a/packages/max/src/plugins/plugin-request/src/index.ts b/packages/max/src/plugins/plugin-request/src/index.ts new file mode 100644 index 00000000..6119f2a3 --- /dev/null +++ b/packages/max/src/plugins/plugin-request/src/index.ts @@ -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; + }); +}; diff --git a/packages/max/src/plugins/plugin-request/src/withTmpPath.ts b/packages/max/src/plugins/plugin-request/src/withTmpPath.ts new file mode 100644 index 00000000..b69f6f48 --- /dev/null +++ b/packages/max/src/plugins/plugin-request/src/withTmpPath.ts @@ -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, + ), + ); +} diff --git a/packages/max/src/plugins/plugin-x/src/astUtils.ts b/packages/max/src/plugins/plugin-x/src/astUtils.ts new file mode 100644 index 00000000..7daa5185 --- /dev/null +++ b/packages/max/src/plugins/plugin-x/src/astUtils.ts @@ -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; +} diff --git a/packages/max/src/plugins/plugin-x/src/constants.ts b/packages/max/src/plugins/plugin-x/src/constants.ts new file mode 100644 index 00000000..5bd85de3 --- /dev/null +++ b/packages/max/src/plugins/plugin-x/src/constants.ts @@ -0,0 +1 @@ +export const INULA_KEYS = ['create', 'use', 'clear']; diff --git a/packages/max/src/plugins/plugin-x/src/index.ts b/packages/max/src/plugins/plugin-x/src/index.ts new file mode 100644 index 00000000..d2775a00 --- /dev/null +++ b/packages/max/src/plugins/plugin-x/src/index.ts @@ -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: [], + }); +} diff --git a/packages/max/src/plugins/plugin-x/src/storesUtils.ts b/packages/max/src/plugins/plugin-x/src/storesUtils.ts new file mode 100644 index 00000000..ee9cca8f --- /dev/null +++ b/packages/max/src/plugins/plugin-x/src/storesUtils.ts @@ -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 = 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(); + const ast = parser.parse(result.code, { + sourceType: 'module', + sourceFilename: this.file, + plugins: [], + }); + // TODO: use sort + sort; + traverse(ast, { + CallExpression: (path: Babel.NodePath) => { + 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, + ) => { + 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) => { + 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')} +`; + } +} diff --git a/packages/max/src/plugins/plugin-x/src/withTmpPath.ts b/packages/max/src/plugins/plugin-x/src/withTmpPath.ts new file mode 100644 index 00000000..b69f6f48 --- /dev/null +++ b/packages/max/src/plugins/plugin-x/src/withTmpPath.ts @@ -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, + ), + ); +} diff --git a/packages/max/src/plugins/preset-inula/assets/bundle-status.html b/packages/max/src/plugins/preset-inula/assets/bundle-status.html new file mode 100644 index 00000000..8ba23665 --- /dev/null +++ b/packages/max/src/plugins/preset-inula/assets/bundle-status.html @@ -0,0 +1,240 @@ + + + + + + + Inula Loading... + + + + + +
+
+

编译中...

+ +

+

+
+ + + + diff --git a/packages/max/src/plugins/preset-inula/src/commands/build.ts b/packages/max/src/plugins/preset-inula/src/commands/build.ts new file mode 100644 index 00000000..ca10533a --- /dev/null +++ b/packages/max/src/plugins/preset-inula/src/commands/build.ts @@ -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 + }, + }); +}; diff --git a/packages/max/src/plugins/preset-inula/src/commands/dev.ts b/packages/max/src/plugins/preset-inula/src/commands/dev.ts new file mode 100644 index 00000000..6c141d1b --- /dev/null +++ b/packages/max/src/plugins/preset-inula/src/commands/dev.ts @@ -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(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; + }); +}; diff --git a/packages/max/src/plugins/preset-inula/src/commands/help.ts b/packages/max/src/plugins/preset-inula/src/commands/help.ts new file mode 100644 index 00000000..cafb025b --- /dev/null +++ b/packages/max/src/plugins/preset-inula/src/commands/help.ts @@ -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 [options] + +Commands: + +${getDeps(commands)} +`); + console.log( + `Run \`${chalk.bold( + 'inula help ', + )}\` 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'); + } +}; diff --git a/packages/max/src/plugins/preset-inula/src/commands/version.ts b/packages/max/src/plugins/preset-inula/src/commands/version.ts new file mode 100644 index 00000000..af7a7093 --- /dev/null +++ b/packages/max/src/plugins/preset-inula/src/commands/version.ts @@ -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; + }, + }); +}; diff --git a/packages/max/src/plugins/preset-inula/src/config/inulaconfig.ts b/packages/max/src/plugins/preset-inula/src/config/inulaconfig.ts new file mode 100644 index 00000000..f2f4e0e1 --- /dev/null +++ b/packages/max/src/plugins/preset-inula/src/config/inulaconfig.ts @@ -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 = { + 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; + }); +}; diff --git a/packages/max/src/plugins/preset-inula/src/constants.ts b/packages/max/src/plugins/preset-inula/src/constants.ts new file mode 100644 index 00000000..bffa86ba --- /dev/null +++ b/packages/max/src/plugins/preset-inula/src/constants.ts @@ -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); diff --git a/packages/max/src/plugins/preset-inula/src/favicon.ico b/packages/max/src/plugins/preset-inula/src/favicon.ico new file mode 100644 index 00000000..dc16ca39 Binary files /dev/null and b/packages/max/src/plugins/preset-inula/src/favicon.ico differ diff --git a/packages/max/src/plugins/preset-inula/src/features/iloading.ts b/packages/max/src/plugins/preset-inula/src/features/iloading.ts new file mode 100644 index 00000000..fef1faf4 --- /dev/null +++ b/packages/max/src/plugins/preset-inula/src/features/iloading.ts @@ -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'), + ), + }); +}; diff --git a/packages/max/src/plugins/preset-inula/src/features/tmpFiles.ts b/packages/max/src/plugins/preset-inula/src/features/tmpFiles.ts new file mode 100644 index 00000000..f3718c8a --- /dev/null +++ b/packages/max/src/plugins/preset-inula/src/features/tmpFiles.ts @@ -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 & { 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 + 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 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 ; +} + `, + }); + + // 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>((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, + }); +}; diff --git a/packages/max/src/plugins/preset-inula/src/index.test.ts b/packages/max/src/plugins/preset-inula/src/index.test.ts new file mode 100644 index 00000000..d7376d8e --- /dev/null +++ b/packages/max/src/plugins/preset-inula/src/index.test.ts @@ -0,0 +1,5 @@ +import index from './index'; + +test('normal', () => { + expect(index()).toEqual('@aluni/preset-inula'); +}); diff --git a/packages/max/src/plugins/preset-inula/src/index.ts b/packages/max/src/plugins/preset-inula/src/index.ts new file mode 100644 index 00000000..7b1bb088 --- /dev/null +++ b/packages/max/src/plugins/preset-inula/src/index.ts @@ -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), + }; +}; diff --git a/packages/max/src/plugins/preset-inula/src/utils/resolveProjectDep.ts b/packages/max/src/plugins/preset-inula/src/utils/resolveProjectDep.ts new file mode 100644 index 00000000..55b20af1 --- /dev/null +++ b/packages/max/src/plugins/preset-inula/src/utils/resolveProjectDep.ts @@ -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, + }), + ); + } +} diff --git a/packages/max/src/requireHook.ts b/packages/max/src/requireHook.ts new file mode 100644 index 00000000..c9ef8fe1 --- /dev/null +++ b/packages/max/src/requireHook.ts @@ -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); +}; diff --git a/packages/max/src/service.ts b/packages/max/src/service.ts new file mode 100644 index 00000000..59552786 --- /dev/null +++ b/packages/max/src/service.ts @@ -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 }); + } +} diff --git a/packages/max/tsconfig.json b/packages/max/tsconfig.json new file mode 100644 index 00000000..70cf4fa4 --- /dev/null +++ b/packages/max/tsconfig.json @@ -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" + ] +}