From f0f2f818f647bbbdbaa4d64ac205c5cbb651daf3 Mon Sep 17 00:00:00 2001 From: sorrycc Date: Thu, 14 Nov 2024 11:00:09 +0800 Subject: [PATCH] refactor: config --- CONTRIBUTING.md | 2 +- README.md | 2 +- src/cli.ts | 2 +- src/config.ts | 98 ---------------------------------------- src/config/config.ts | 50 ++++++++++++++++++++ src/config/types.ts | 50 ++++++++++++++++++++ src/fishkit/server.ts | 2 +- src/generate/generate.ts | 1 - src/plugin_manager.ts | 67 +++++++++++++++++++++++++++ src/types/index.ts | 2 +- 10 files changed, 172 insertions(+), 104 deletions(-) delete mode 100644 src/config.ts create mode 100644 src/config/config.ts create mode 100644 src/config/types.ts create mode 100644 src/plugin_manager.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5476af..6b889af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,7 +90,7 @@ $ pnpm release:create-tnf We use [Prettier](https://prettier.io/) to format the code, please run `pnpm format` to format the code. And we also have some other rules: - Do not use specifiers for `fs` and `path` modules. -- Do use `pathe` instead of `path` module. +- Do use `pathe` instead of `path` module for windows compatibility. ```ts // bad diff --git a/README.md b/README.md index acf7f54..8e7a46a 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Tnf, ~~the north face~~, the next framework. Tnf is focused on simple, performan - Conventional global style with `src/global.{less,css}`. - Less, CSS Modules support built-in. - Tailwind CSS support built-in. -- [ ] Framework unplugin which is compatible with umi and other frameworks. +- Framework unified plugin system which is compatible with umi and other frameworks. - [ ] Security built-in. Including doctor rules which is used in Ant Group. - [ ] Support SSR, API routes and server functions. - [ ] AI based generator and other features. diff --git a/src/cli.ts b/src/cli.ts index aaff0a3..6071463 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,7 +1,7 @@ import assert from 'assert'; import path from 'pathe'; import yargsParser from 'yargs-parser'; -import { loadConfig } from './config'; +import { loadConfig } from './config/config'; import { FRAMEWORK_NAME, MIN_NODE_VERSION } from './constants'; import { checkVersion, setNoDeprecation, setNodeTitle } from './fishkit/node'; import { PluginManager } from './plugin/plugin_manager'; diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index 087ae98..0000000 --- a/src/config.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - loadConfig as loadC12Config, - watchConfig as watchC12Config, -} from 'c12'; -import { z } from 'zod'; -import { CONFIG_FILE } from './constants'; -import { PluginSchema } from './plugin/types'; - -const ConfigSchema = z.object({ - externals: z.record(z.string()).optional(), - devServer: z - .object({ - port: z.number().optional(), - https: z - .object({ - hosts: z.array(z.string()).optional(), - }) - .optional(), - ip: z.string().optional(), - host: z.string().optional(), - }) - .optional(), - less: z - .object({ - modifyVars: z.any().optional(), - globalVars: z.any().optional(), - math: z.any().optional(), - sourceMap: z.any(), - plugins: z.array(z.any()).optional(), - }) - .optional(), - plugins: z.array(PluginSchema).optional(), - router: z - .object({ - defaultPreload: z.enum(['intent', 'render', 'viewport']).optional(), - defaultPreloadDelay: z.number().optional(), - devtool: z - .union([ - z.object({ - options: z.object({ - initialIsOpen: z.boolean().optional(), - position: z - .enum(['bottom-left', 'bottom-right', 'top-left', 'top-right']) - .optional(), - }), - }), - z.literal(false), - ]) - .optional(), - }) - .optional(), - tailwindcss: z.boolean().optional(), -}); - -export type Config = z.infer; - -interface ConfigOpts { - cwd: string; - defaults?: Partial; - overrides?: Partial; -} - -export async function loadConfig(opts: ConfigOpts): Promise { - const { config: rawConfig } = await loadC12Config(createLoadConfigOpts(opts)); - const result = ConfigSchema.safeParse(rawConfig); - if (!result.success) { - throw new Error(`Invalid configuration: ${result.error.message}`); - } - return result.data; -} - -export function watchConfig(opts: ConfigOpts) { - return watchC12Config({ - ...createLoadConfigOpts(opts), - onWatch(event) { - console.log(event); - }, - onUpdate({ oldConfig, newConfig, getDiff }) { - const result = ConfigSchema.safeParse(newConfig); - if (!result.success) { - console.error(`Invalid configuration: ${result.error.message}`); - return; - } - console.log('onUpdate', oldConfig, result.data, getDiff()); - }, - }); -} - -function createLoadConfigOpts({ cwd, defaults, overrides }: ConfigOpts) { - return { - cwd, - configFile: CONFIG_FILE, - rcFile: false as const, - globalRc: false as const, - defaults, - overrides, - }; -} diff --git a/src/config/config.ts b/src/config/config.ts new file mode 100644 index 0000000..f38fc33 --- /dev/null +++ b/src/config/config.ts @@ -0,0 +1,50 @@ +import { + loadConfig as loadC12Config, + watchConfig as watchC12Config, +} from 'c12'; +import { CONFIG_FILE } from '../constants'; +import type { Config } from './types'; +import { ConfigSchema } from './types'; + +interface ConfigOpts { + cwd: string; + defaults?: Partial; + overrides?: Partial; +} + +export async function loadConfig(opts: ConfigOpts): Promise { + const { config: rawConfig } = await loadC12Config(createLoadConfigOpts(opts)); + const result = ConfigSchema.safeParse(rawConfig); + if (!result.success) { + throw new Error(`Invalid configuration: ${result.error.message}`); + } + return result.data; +} + +export function watchConfig(opts: ConfigOpts) { + return watchC12Config({ + ...createLoadConfigOpts(opts), + onWatch(event) { + console.log(event); + }, + onUpdate({ oldConfig, newConfig, getDiff }) { + const result = ConfigSchema.safeParse(newConfig); + if (!result.success) { + console.error(`Invalid configuration: ${result.error.message}`); + return; + } + console.log('onUpdate', oldConfig, result.data, getDiff()); + }, + }); +} + +function createLoadConfigOpts({ cwd, defaults, overrides }: ConfigOpts) { + return { + cwd, + configFile: CONFIG_FILE, + rcFile: false as const, + globalRc: false as const, + defaults, + overrides, + }; +} diff --git a/src/config/types.ts b/src/config/types.ts new file mode 100644 index 0000000..2028832 --- /dev/null +++ b/src/config/types.ts @@ -0,0 +1,50 @@ +import { z } from 'zod'; +import { PluginSchema } from '../plugin/types'; + +export const ConfigSchema = z.object({ + externals: z.record(z.string()).optional(), + devServer: z + .object({ + port: z.number().optional(), + https: z + .object({ + hosts: z.array(z.string()).optional(), + }) + .optional(), + ip: z.string().optional(), + host: z.string().optional(), + }) + .optional(), + less: z + .object({ + modifyVars: z.any().optional(), + globalVars: z.any().optional(), + math: z.any().optional(), + sourceMap: z.any(), + plugins: z.array(z.any()).optional(), + }) + .optional(), + plugins: z.array(PluginSchema).optional(), + router: z + .object({ + defaultPreload: z.enum(['intent', 'render', 'viewport']).optional(), + defaultPreloadDelay: z.number().optional(), + devtool: z + .union([ + z.object({ + options: z.object({ + initialIsOpen: z.boolean().optional(), + position: z + .enum(['bottom-left', 'bottom-right', 'top-left', 'top-right']) + .optional(), + }), + }), + z.literal(false), + ]) + .optional(), + }) + .optional(), + tailwindcss: z.boolean().optional(), +}); + +export type Config = z.infer; diff --git a/src/fishkit/server.ts b/src/fishkit/server.ts index ce04ba8..fa7ba00 100644 --- a/src/fishkit/server.ts +++ b/src/fishkit/server.ts @@ -6,7 +6,7 @@ import proxy from 'express-http-proxy'; import { getPort } from 'get-port-please'; import http from 'http'; import { createProxyMiddleware } from 'http-proxy-middleware'; -import { type Config } from '../config'; +import type { Config } from '../config/types'; import { DEFAULT_PORT } from '../constants'; import { createHttpsServer } from './https'; diff --git a/src/generate/generate.ts b/src/generate/generate.ts index 9a5890a..f1cc208 100644 --- a/src/generate/generate.ts +++ b/src/generate/generate.ts @@ -1,4 +1,3 @@ -import type yargsParser from 'yargs-parser'; import type { Context } from '../types'; import { generatePage } from './generate_page'; diff --git a/src/plugin_manager.ts b/src/plugin_manager.ts new file mode 100644 index 0000000..998b07e --- /dev/null +++ b/src/plugin_manager.ts @@ -0,0 +1,67 @@ +import type { Plugin } from './plugin/types'; + +type PluginKey = keyof Plugin; + +enum HookType { + First = 'first', + Series = 'series', + Parallel = 'parallel', +} + +export class PluginManager { + #plugins: Plugin[] = []; + constructor(rawPlugins: Plugin[]) { + this.#plugins = [ + ...rawPlugins.filter((p) => p.enforce === 'pre'), + ...rawPlugins.filter((p) => !p.enforce), + ...rawPlugins.filter((p) => p.enforce === 'post'), + ]; + } + + async apply({ + hook, + args, + type = HookType.Series, + }: { + hook: PluginKey; + args: any; + type: HookType; + }) { + const plugins = this.#plugins.filter((p) => !!p[hook]); + + if (type === HookType.First) { + for (const plugin of plugins) { + const hookFn = plugin[hook]; + if (typeof hookFn === 'function') { + const result = await hookFn(args); + if (result != null) { + return result; + } + } + } + return null; + } + + if (type === HookType.Parallel) { + const results = await Promise.all( + plugins.map((p) => { + const hookFn = p[hook]; + if (typeof hookFn === 'function') { + return hookFn(args); + } + return null; + }), + ); + return results.filter((r) => r != null); + } + + let result = args; + for (const plugin of plugins) { + const hookFn = plugin[hook]; + if (typeof hookFn === 'function') { + result = await hookFn(result); + } + } + return result; + } +} diff --git a/src/types/index.ts b/src/types/index.ts index c80b71f..be9f9c6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,5 +1,5 @@ import type yargsParser from 'yargs-parser'; -import type { Config } from '../config'; +import type { Config } from '../config/types'; import type { PluginManager } from '../plugin/plugin_manager'; import type { Plugin } from '../plugin/types';