diff --git a/packages/cli/src/__tests__/plugins.test.ts b/packages/cli/src/__tests__/plugins.test.ts new file mode 100644 index 0000000000..e9e98eca7a --- /dev/null +++ b/packages/cli/src/__tests__/plugins.test.ts @@ -0,0 +1,124 @@ +import { describe, test, expect } from 'vitest'; +import { MitosisConfig } from '@builder.io/mitosis'; +import { formatHook, runHook } from '../build/build'; + +const beforeBuild = (c) => { + console.log(`beforeBuild ${c}`); +}; +const afterbuild = (c, f) => { + console.log(`afterbuild ${c} ${f}`); +}; +const beforeBuildSecond = (c) => { + console.log(`beforeBuildSecond ${c}`); +}; +const afterbuildSecond = (c, f) => { + console.log(f); + console.log(`afterbuildSecond ${c} ${f}`); +}; + +const objPluginConfig = { + plugins: { + order: 1, + beforeBuild, + afterbuild, + }, +}; +const fnPluginConfig = { + plugins: () => ({ + order: 1, + beforeBuild, + afterbuild, + }), +}; +const mixPluginConfig = { + plugins: [ + () => ({ + name: 'second', + order: 2, + beforeBuild: beforeBuildSecond, + afterbuild: afterbuildSecond, + }), + { + name: 'first', + order: 1, + beforeBuild, + afterbuild, + }, + ], +}; + +describe('mitosis plugin test', () => { + test('formatHook test objPlugin', () => { + const plugins = formatHook(objPluginConfig as MitosisConfig); + expect(plugins).toEqual([ + { + order: 1, + beforeBuild, + afterbuild, + }, + ]); + }); + test('formatHook test fnPlugin', () => { + const plugins = formatHook(fnPluginConfig as MitosisConfig); + expect(plugins).toEqual([ + { + order: 1, + beforeBuild, + afterbuild, + }, + ]); + }); + test('formatHook test mixPlugin', () => { + const plugins = formatHook(mixPluginConfig as MitosisConfig); + expect(plugins).toEqual([ + { + name: 'first', + order: 1, + beforeBuild, + afterbuild, + }, + { + name: 'second', + order: 2, + beforeBuild: beforeBuildSecond, + afterbuild: afterbuildSecond, + }, + ]); + expect(plugins).not.toEqual([ + { + name: 'second', + order: 2, + beforeBuild: beforeBuildSecond, + afterbuild: afterbuildSecond, + }, + { + name: 'first', + order: 1, + beforeBuild, + afterbuild, + }, + ]); + }); + test('runHook test', async () => { + const _log = console.log; + const logs = []; + console.log = (str) => { + logs.push(str); + }; + const c = { test: 1 }; + const f = { test: 2 }; + const plugins = formatHook(mixPluginConfig as MitosisConfig); + await runHook('beforeBuild', { plugins } as MitosisConfig)(c); + expect(logs).toEqual([`beforeBuild ${c}`, `beforeBuildSecond ${c}`]); + await runHook('afterbuild', { plugins } as MitosisConfig)(c, f); + + expect(logs).toEqual([ + `beforeBuild ${c}`, + `beforeBuildSecond ${c}`, + `afterbuild ${c} ${f}`, + f, + `afterbuildSecond ${c} ${f}`, + ]); + console.log = _log; + }); +}); diff --git a/packages/cli/src/build/build.ts b/packages/cli/src/build/build.ts index d0fa6aee87..0af85ddd07 100644 --- a/packages/cli/src/build/build.ts +++ b/packages/cli/src/build/build.ts @@ -24,8 +24,12 @@ import { MitosisConfig, parseJsx, Target, - TranspilerGenerator, parseSvelte, + TargetContext, + OutputFiles, + MitosisPlugin, + MitosisPluginFn, + HookType, } from '@builder.io/mitosis'; import debug from 'debug'; import { flow, pipe } from 'fp-ts/lib/function'; @@ -69,6 +73,25 @@ const DEFAULT_CONFIG: Partial = { getTargetPath, }; +export const formatHook = (config?: MitosisConfig): Partial[] => { + const plugins = config?.plugins; + if (!plugins) return []; + + let pluginList = plugins; + if (!Array.isArray(plugins)) { + pluginList = [plugins]; + } + + return (pluginList as Array>) + .map((plugin) => { + if (typeof plugin === 'function') { + return plugin(); + } + return plugin; + }) + .sort((a, b) => (a.order || 0) - (b.order || 0)); +}; + const getOptions = (config?: MitosisConfig): MitosisConfig => ({ ...DEFAULT_CONFIG, ...config, @@ -76,6 +99,7 @@ const getOptions = (config?: MitosisConfig): MitosisConfig => ({ ...DEFAULT_CONFIG.options, ...config?.options, }, + plugins: formatHook(config), }); async function clean(options: MitosisConfig) { @@ -192,12 +216,6 @@ const getMitosisComponentJSONs = async (options: MitosisConfig): Promise; - outputPath: string; -} - interface TargetContextWithConfig extends TargetContext { options: MitosisConfig; } @@ -216,6 +234,20 @@ const buildAndOutputNonComponentFiles = async (targetContext: TargetContextWithC return await outputNonComponentFiles({ ...targetContext, files }); }; +export function runHook(hook: string, config: MitosisConfig) { + const plugins = config.plugins as Partial[]; + const debugTarget = debug(`mitosis:${hook}`); + + return async (...params) => { + for (let plugin of plugins) { + if (!plugin[hook]) continue; + debugTarget(`before run ${plugin.name} ${hook} hook...`); + await plugin[hook](...params); + debugTarget(`run $${plugin.name} ${hook} hook done`); + } + }; +} + export async function build(config?: MitosisConfig) { // merge default options const options = getOptions(config); @@ -227,6 +259,7 @@ export async function build(config?: MitosisConfig) { const mitosisComponents = await getMitosisComponentJSONs(options); const targetContexts = getTargetContexts(options); + await runHook(HookType.BeforeBuild, options)(targetContexts); await Promise.all( targetContexts.map(async (targetContext) => { @@ -238,7 +271,10 @@ export async function build(config?: MitosisConfig) { buildAndOutputNonComponentFiles({ ...targetContext, options }), buildAndOutputComponentFiles({ ...targetContext, options, files }), ]); - + await runHook(HookType.Afterbuild, options)(targetContext, { + componentFiles: x[1], + nonComponentFiles: x[0], + }); console.info( `Mitosis: ${targetContext.target}: generated ${x[1].length} components, ${x[0].length} regular files.`, ); @@ -326,7 +362,7 @@ async function buildAndOutputComponentFiles({ options, generator, outputPath, -}: TargetContextWithConfig & { files: ParsedMitosisJson[] }) { +}: TargetContextWithConfig & { files: ParsedMitosisJson[] }): Promise { const debugTarget = debug(`mitosis:${target}`); const shouldOutputTypescript = checkShouldOutputTypeScript({ options, target }); @@ -368,6 +404,7 @@ async function buildAndOutputComponentFiles({ const outputDir = `${options.dest}/${outputPath}`; await outputFile(`${outputDir}/${outputFilePath}`, transpiled); + return { outputDir, outputFilePath }; }); return await Promise.all(output); } @@ -387,13 +424,15 @@ const outputNonComponentFiles = async ({ }: TargetContext & { files: { path: string; output: string }[]; options: MitosisConfig; -}) => { +}): Promise => { const extension = getNonComponentFileExtension({ target, options }); - const folderPath = `${options.dest}/${outputPath}`; + const outputDir = `${options.dest}/${outputPath}`; return await Promise.all( - files.map(({ path, output }) => - outputFile(`${folderPath}/${path.replace(/\.tsx?$/, extension)}`, output), - ), + files.map(async ({ path, output }) => { + const outputFilePath = path.replace(/\.tsx?$/, extension); + await outputFile(`${outputDir}/${outputFilePath}`, output); + return { outputDir, outputFilePath }; + }), ); }; diff --git a/packages/core/src/types/config.ts b/packages/core/src/types/config.ts index a0e231e6ed..3f718ab4f0 100644 --- a/packages/core/src/types/config.ts +++ b/packages/core/src/types/config.ts @@ -1,4 +1,5 @@ import { MitosisComponent } from './mitosis-component'; +import { TranspilerGenerator } from './transpiler'; export type Format = 'esm' | 'cjs'; export type Language = 'js' | 'ts'; @@ -14,6 +15,37 @@ export type GeneratorOptions = { }; }; +export interface TargetContext { + target: Target; + generator: TranspilerGenerator>; + outputPath: string; +} + +export interface OutputFiles { + outputDir: string; + outputFilePath: string; +} + +export interface MitosisPlugin { + name: string; + order: number; + [HookType.BeforeBuild]: (TargetContexts: TargetContext[]) => void | Promise; + [HookType.Afterbuild]: ( + TargetContext: TargetContext, + files: { + componentFiles: OutputFiles[]; + nonComponentFiles: OutputFiles[]; + }, + ) => void | Promise; +} + +export type MitosisPluginFn = () => Partial; + +export enum HookType { + BeforeBuild = 'beforeBuild', + Afterbuild = 'afterbuild', +} + export type MitosisConfig = { /** * List of targets to compile to. @@ -58,6 +90,14 @@ export type MitosisConfig = { * ``` */ options: Partial; + + /** + * hooks + */ + plugins?: + | Partial + | (() => Partial) + | Array | MitosisPluginFn>; /** * Configure a custom parser function which takes a string and returns MitosisJSON * Defaults to the JSXParser of this project (src/parsers/jsx)