From 3e664001931902f13ddffa95c892718bb3de5594 Mon Sep 17 00:00:00 2001 From: Jinbao1001 Date: Fri, 9 Aug 2024 15:33:47 +0800 Subject: [PATCH] feat: bunless parallel (#771) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: bunless parallel * fix: type error * fix: raw * fix: type * chore: 补充单测 * Update ci.yml --- .github/workflows/ci.yml | 3 + docs/config.md | 7 + package.json | 1 + pnpm-lock.yaml | 32 ++++ src/builder/bundless/index.ts | 150 ++++++++++------- src/builder/bundless/loaders/index.ts | 157 +++++++++++------- .../bundless/loaders/javascript/babel.ts | 4 +- .../bundless/loaders/javascript/esbuild.ts | 4 +- .../bundless/loaders/javascript/index.ts | 10 +- .../bundless/loaders/javascript/swc.ts | 4 +- src/builder/bundless/loaders/types.ts | 9 +- src/builder/bundless/parallelLoader.ts | 23 +++ src/builder/config.ts | 2 + src/features/configBuilder/configBuilder.ts | 10 +- src/features/configPlugins/schema.ts | 1 + src/types.ts | 7 +- tests/parallel.test.ts | 68 ++++++++ 17 files changed, 353 insertions(+), 139 deletions(-) create mode 100644 src/builder/bundless/parallelLoader.ts create mode 100644 tests/parallel.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06acc3fd..0da9d7f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,9 @@ jobs: - name: Install dependencies run: pnpm i + - name: Build Father Projext + run: pnpm run build + - name: Type check run: pnpm tsc --noEmit diff --git a/docs/config.md b/docs/config.md index 5449d2a2..fa89f5dc 100644 --- a/docs/config.md +++ b/docs/config.md @@ -136,6 +136,13 @@ export default { 配置转换过程中需要忽略的文件,支持 glob 表达式,被匹配的文件将不会输出到产物目录。另外,father 会默认忽略源码目录中所有的 Markdown 文件和测试文件。 +#### parallel + +- 类型:`boolean` +- 默认值:`false` + +指定是否开启并发编译,默认关闭。 + ### umd - 类型:`object` diff --git a/package.json b/package.json index 0cde2b57..c0fb529d 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "file-system-cache": "2.0.0", "loader-runner": "4.2.0", "minimatch": "3.1.2", + "piscina": "^4.6.1", "tsconfig-paths": "4.0.0", "typescript": "~5.3.3", "typescript-transform-paths": "3.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02a25687..f6d68a78 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: minimatch: specifier: 3.1.2 version: 3.1.2 + piscina: + specifier: ^4.6.1 + version: 4.6.1 tsconfig-paths: specifier: 4.0.0 version: 4.0.0 @@ -7696,10 +7699,26 @@ packages: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} dev: false + /nice-napi@1.0.2: + resolution: {integrity: sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==} + os: ['!win32'] + requiresBuild: true + dependencies: + node-addon-api: 3.2.1 + node-gyp-build: 4.8.1 + dev: false + optional: true + /node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} dev: false + /node-addon-api@3.2.1: + resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} + requiresBuild: true + dev: false + optional: true + /node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -7726,6 +7745,13 @@ packages: formdata-polyfill: 4.0.10 dev: true + /node-gyp-build@4.8.1: + resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} + hasBin: true + requiresBuild: true + dev: false + optional: true + /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true @@ -8079,6 +8105,12 @@ packages: engines: {node: '>= 6'} dev: true + /piscina@4.6.1: + resolution: {integrity: sha512-z30AwWGtQE+Apr+2WBZensP2lIvwoaMcOPkQlIEmSGMJNUvaYACylPYrQM6wSdUNJlnDVMSpLv7xTMJqlVshOA==} + optionalDependencies: + nice-napi: 1.0.2 + dev: false + /pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} diff --git a/src/builder/bundless/index.ts b/src/builder/bundless/index.ts index a6b02117..49f20403 100644 --- a/src/builder/bundless/index.ts +++ b/src/builder/bundless/index.ts @@ -1,12 +1,4 @@ -import { - chalk, - chokidar, - debug, - glob, - lodash, - rimraf, - winPath, -} from '@umijs/utils'; +import { chalk, chokidar, debug, glob, lodash, rimraf } from '@umijs/utils'; import fs from 'fs'; import path from 'path'; import { @@ -17,9 +9,67 @@ import { import { logger } from '../../utils'; import type { BundlessConfigProvider } from '../config'; import getDeclarations from './dts'; +import type { ILoaderArgs } from './loaders'; import runLoaders from './loaders'; +import { IJSTransformer, IJSTransformerFn } from './loaders/types'; +import createParallelLoader from './parallelLoader'; const debugLog = debug(DEBUG_BUNDLESS_NAME); +let parallelLoader: ReturnType | undefined; +/** + * loader item type + */ +interface ILoaderItem { + id: string; + test: string | RegExp | ((path: string) => boolean); + loader: string; + options?: Record; +} + +export type Loaders = ILoaderItem[]; + +const loaders: ILoaderItem[] = []; + +/** + * add loader + * @param item loader item + */ +export function addLoader(item: ILoaderItem) { + // only support simple test type currently, because the webpack condition is too complex + // refer: https://github.com/webpack/webpack/blob/0f6c78cca174a73184fdc0d9c9c2bd376b48557c/lib/rules/RuleSetCompiler.js#L211 + if ( + !['string', 'function'].includes(typeof item.test) && + !(item.test instanceof RegExp) + ) { + throw new Error( + `Unsupported loader test in \`${item.id}\`, only string, function and regular expression are available.`, + ); + } + + loaders.push(item); +} + +const transformers: Record = {}; + +export interface ITransformerItem { + id: string; + transformer: string; +} + +export type Transformers = Record; + +/** + * add javascript transformer + * @param item + */ +export function addTransformer(item: ITransformerItem) { + const mod = require(item.transformer); + const transformer: IJSTransformerFn = mod.default || mod; + transformers[item.id] = { + fn: transformer, + resolvePath: item.transformer, + }; +} /** * replace extension for path @@ -44,7 +94,8 @@ async function transformFiles( ) { try { let count = 0; - const declarationFileMap = new Map(); + let bundlessPromises = []; + let declarationFileMap = new Map(); // process all matched items for (let item of files) { @@ -63,67 +114,51 @@ async function transformFiles( if (!fs.existsSync(parentPath)) { fs.mkdirSync(parentPath, { recursive: true }); } - - // get result from loaders - const result = await runLoaders(itemAbsPath, { - config, - pkg: opts.configProvider.pkg, - cwd: opts.cwd, - itemDistAbsPath, - }); - - if (result) { - // update ext if loader specified - if (result.options.ext) { - itemDistPath = replacePathExt(itemDistPath, result.options.ext); - itemDistAbsPath = replacePathExt( - itemDistAbsPath, - result.options.ext, - ); - } - - // prepare for declaration - if (result.options.declaration) { - // use winPath because ts compiler will convert to posix path - declarationFileMap.set(winPath(itemAbsPath), parentPath); - } - - if (result.options.map) { - const map = result.options.map; - const mapLoc = `${itemDistAbsPath}.map`; - - fs.writeFileSync(mapLoc, map); + const loaderArgs: ILoaderArgs = { + fileAbsPath: itemAbsPath, + fileDistPath: itemDistPath, + loaders, + transformers, + opts: { + config, + pkg: opts.configProvider.pkg, + cwd: opts.cwd, + itemDistAbsPath, + }, + }; + if (config.parallel) { + parallelLoader ||= createParallelLoader(); + for (const key in transformers) { + if (loaderArgs.transformers.hasOwnProperty(key)) { + delete transformers[key].fn; + } } - - // distribute file with result - fs.writeFileSync(itemDistAbsPath, result.content); + bundlessPromises.push(parallelLoader.run(loaderArgs)); } else { - // copy file as normal assets - fs.copyFileSync(itemAbsPath, itemDistAbsPath); + bundlessPromises.push(runLoaders(loaderArgs)); } - - logger.quietExpect.event( - `Bundless ${chalk.gray(item)} to ${chalk.gray(itemDistPath)}${result?.options.declaration ? ' (with declaration)' : '' - }`, - ); count += 1; } else { debugLog(`No config matches ${chalk.gray(item)}, skip`); } } + const results = await Promise.all(bundlessPromises); + lodash.forEach(results, (item) => { + if (item) { + declarationFileMap.set(item[0], item[1]); + } + }); if (declarationFileMap.size) { logger.quietExpect.event( `Generate declaration file${declarationFileMap.size > 1 ? 's' : ''}...`, ); - const declarations = await getDeclarations( [...declarationFileMap.keys()], { cwd: opts.cwd, }, ); - declarations.forEach((item) => { fs.writeFileSync( path.join(declarationFileMap.get(item.sourceFile)!, item.file), @@ -177,7 +212,8 @@ async function bundless( if (!opts.watch) { // output result for normal mode logger.quietExpect.event( - `Transformed successfully in ${Date.now() - startTime + `Transformed successfully in ${ + Date.now() - startTime } ms (${count} files)`, ); } else { @@ -229,10 +265,10 @@ async function bundless( // TODO: collect real emit files const relatedFiles = isTsFile ? [ - replacePathExt(fileDistAbsPath, '.js'), - replacePathExt(fileDistAbsPath, '.d.ts'), - replacePathExt(fileDistAbsPath, '.d.ts.map'), - ] + replacePathExt(fileDistAbsPath, '.js'), + replacePathExt(fileDistAbsPath, '.d.ts'), + replacePathExt(fileDistAbsPath, '.d.ts.map'), + ] : [fileDistAbsPath]; const relatedMainFile = relatedFiles.find((item) => fs.existsSync(item), diff --git a/src/builder/bundless/loaders/index.ts b/src/builder/bundless/loaders/index.ts index d9f558a6..b3dd6e42 100644 --- a/src/builder/bundless/loaders/index.ts +++ b/src/builder/bundless/loaders/index.ts @@ -1,65 +1,88 @@ -import { winPath } from '@umijs/utils'; +import { chalk, winPath } from '@umijs/utils'; import fs from 'fs'; import { runLoaders } from 'loader-runner'; +import path from 'path'; import type { IApi } from '../../../types'; -import { getCache } from '../../../utils'; +import { getCache, logger } from '../../../utils'; import type { IBundlessConfig } from '../../config'; import { getContentHash } from '../../utils'; import { getTsconfig } from '../dts'; import type { IBundlessLoader, ILoaderOutput } from './types'; -/** - * loader item type - */ -export interface ILoaderItem { - id: string; - test: string | RegExp | ((path: string) => boolean); - loader: string; - options?: Record; +import type { Loaders, Transformers } from '..'; + +export interface ILoaderArgs { + fileAbsPath: string; + loaders: Loaders; + fileDistPath: string; + transformers: Transformers; + opts: { + config: IBundlessConfig; + pkg: IApi['pkg']; + cwd: string; + itemDistAbsPath: string; + }; } -const loaders: ILoaderItem[] = []; +function replacePathExt(filePath: string, ext: string) { + const parsed = path.parse(filePath); -/** - * add loader - * @param item loader item - */ -export function addLoader(item: ILoaderItem) { - // only support simple test type currently, because the webpack condition is too complex - // refer: https://github.com/webpack/webpack/blob/0f6c78cca174a73184fdc0d9c9c2bd376b48557c/lib/rules/RuleSetCompiler.js#L211 - if ( - !['string', 'function'].includes(typeof item.test) && - !(item.test instanceof RegExp) - ) { - throw new Error( - `Unsupported loader test in \`${item.id}\`, only string, function and regular expression are available.`, - ); - } + return path.join(parsed.dir, `${parsed.name}${ext}`); +} + +function dealResult(result: any, args: ILoaderArgs) { + let fileDistAbsPath = args.opts.itemDistAbsPath; + if (result) { + // update ext if loader specified + if (result.options.ext) { + fileDistAbsPath = replacePathExt( + args.opts.itemDistAbsPath, + result.options.ext, + ); + } - loaders.push(item); + if (result.options.map) { + const map = result.options.map; + const mapLoc = `${fileDistAbsPath}.map`; + + fs.writeFileSync(mapLoc, map); + } + + // distribute file with result + fs.writeFileSync(fileDistAbsPath, result.content); + } else { + // copy file as normal assets + fs.copyFileSync(args.fileAbsPath, fileDistAbsPath); + } + logger.quietExpect.event( + `Bundless ${chalk.gray(path.basename(fileDistAbsPath))} to ${chalk.gray( + args.fileDistPath, + )}${result?.options.declaration ? ' (with declaration)' : ''}`, + ); + // prepare for declaration + if (result.options.declaration) { + // use winPath because ts compiler will convert to posix path + return [winPath(args.fileAbsPath), path.dirname(fileDistAbsPath)]; + } } /** * loader module base on webpack loader-runner */ -export default async ( - fileAbsPath: string, - opts: { - config: IBundlessConfig; - pkg: IApi['pkg']; - cwd: string; - itemDistAbsPath: string; - }, -) => { +export default async (args: ILoaderArgs) => { const cache = getCache('bundless-loader'); // format: {path:contenthash:config:pkgDeps} const cacheKey = [ - fileAbsPath, - getContentHash(fs.readFileSync(fileAbsPath, 'utf-8')), - JSON.stringify(opts.config), + args.fileAbsPath, + getContentHash(fs.readFileSync(args.fileAbsPath, 'utf-8')), + JSON.stringify(args.opts.config), // use for babel opts generator in src/builder/utils.ts JSON.stringify( - Object.assign({}, opts.pkg.dependencies, opts.pkg.peerDependencies), + Object.assign( + {}, + args.opts.pkg.dependencies, + args.opts.pkg.peerDependencies, + ), ), ].join(':'); const cacheRet = await cache.get(cacheKey, ''); @@ -67,51 +90,54 @@ export default async ( // use cache first /* istanbul ignore if -- @preserve */ if (cacheRet) { - const tsconfig = /\.tsx?$/.test(fileAbsPath) - ? getTsconfig(opts.cwd) + const tsconfig = /\.tsx?$/.test(args.fileAbsPath) + ? getTsconfig(args.opts.cwd) : undefined; - - return Promise.resolve({ - ...cacheRet, - options: { - ...cacheRet.options, - // FIXME: shit code for avoid invalid declaration value when tsconfig changed - declaration: - tsconfig?.options.declaration && - tsconfig?.fileNames.includes(winPath(fileAbsPath)), + const declaration = dealResult( + { + ...cacheRet, + options: { + ...cacheRet.options, + // FIXME: shit code for avoid invalid declaration value when tsconfig changed + declaration: + tsconfig?.options.declaration && + tsconfig?.fileNames.includes(winPath(args.fileAbsPath)), + }, }, - }); + args, + ); + return Promise.resolve(declaration); } // get matched loader by test - const matched = loaders.find((item) => { + const matched = args.loaders.find((item) => { switch (typeof item.test) { case 'string': - return fileAbsPath.startsWith(item.test); + return args.fileAbsPath.startsWith(item.test); case 'function': - return item.test(fileAbsPath); + return item.test(args.fileAbsPath); default: // assume it is RegExp instance - return item.test.test(fileAbsPath); + return item.test.test(args.fileAbsPath); } }); if (matched) { // run matched loader - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { let outputOpts: ILoaderOutput['options'] = {}; - runLoaders( { - resource: fileAbsPath, + resource: args.fileAbsPath, loaders: [{ loader: matched.loader, options: matched.options }], context: { - cwd: opts.cwd, - config: opts.config, - pkg: opts.pkg, - itemDistAbsPath: opts.itemDistAbsPath, + cwd: args.opts.cwd, + config: args.opts.config, + transformers: args.transformers, + pkg: args.opts.pkg, + itemDistAbsPath: args.opts.itemDistAbsPath, setOutputOptions(opts) { outputOpts = opts; }, @@ -130,7 +156,8 @@ export default async ( // save cache then resolve cache.set(cacheKey, ret).then(() => { - resolve(ret); + const declaration = dealResult(ret, args); + resolve(declaration); }); } else { resolve(void 0); @@ -138,5 +165,7 @@ export default async ( }, ); }); + } else { + fs.copyFileSync(args.fileAbsPath, args.opts.itemDistAbsPath); } }; diff --git a/src/builder/bundless/loaders/javascript/babel.ts b/src/builder/bundless/loaders/javascript/babel.ts index 45688ba1..baa94d95 100644 --- a/src/builder/bundless/loaders/javascript/babel.ts +++ b/src/builder/bundless/loaders/javascript/babel.ts @@ -9,7 +9,7 @@ import { getBabelStyledComponentsOpts, getBundlessTargets, } from '../../../utils'; -import type { IJSTransformer } from '../types'; +import type { IJSTransformerFn } from '../types'; /** * parse for stringify define value, use to babel-plugin-transform-define @@ -27,7 +27,7 @@ function getParsedDefine(define: Record) { /** * babel transformer */ -const babelTransformer: IJSTransformer = function (content) { +const babelTransformer: IJSTransformerFn = function (content) { const { extraBabelPlugins = [], extraBabelPresets = [], diff --git a/src/builder/bundless/loaders/javascript/esbuild.ts b/src/builder/bundless/loaders/javascript/esbuild.ts index 4cbdcb89..e9d7f61a 100644 --- a/src/builder/bundless/loaders/javascript/esbuild.ts +++ b/src/builder/bundless/loaders/javascript/esbuild.ts @@ -3,7 +3,7 @@ import { build } from 'esbuild'; import path from 'path'; import { IFatherBundlessConfig } from '../../../../types'; import { getBundlessTargets } from '../../../utils'; -import type { IJSTransformer } from '../types'; +import type { IJSTransformerFn } from '../types'; /** * create a replacer for transform alias path to relative path @@ -42,7 +42,7 @@ function createAliasReplacer(opts: { alias: IFatherBundlessConfig['alias'] }) { /** * esbuild transformer */ -const esbuildTransformer: IJSTransformer = async function () { +const esbuildTransformer: IJSTransformerFn = async function () { const replacer = createAliasReplacer({ alias: this.config.alias }); let { outputFiles } = await build({ diff --git a/src/builder/bundless/loaders/javascript/index.ts b/src/builder/bundless/loaders/javascript/index.ts index b3da638d..20c4b33e 100644 --- a/src/builder/bundless/loaders/javascript/index.ts +++ b/src/builder/bundless/loaders/javascript/index.ts @@ -16,7 +16,6 @@ export interface ITransformerItem { export function addTransformer(item: ITransformerItem) { const mod = require(item.transformer); const transformer: IJSTransformer = mod.default || mod; - transformers[item.id] = transformer; } @@ -24,7 +23,12 @@ export function addTransformer(item: ITransformerItem) { * builtin javascript loader */ const jsLoader: IBundlessLoader = function (content) { - const transformer = transformers[this.config.transformer!]; + const transformer = this.transformers[this.config.transformer!]; + if (typeof transformer.fn !== 'function') { + const mod = require(this.transformers[this.config.transformer!] + .resolvePath as string); + transformer.fn = mod.default || mod; + } const outputOpts: ILoaderOutput['options'] = {}; // specify output ext for non-js file @@ -43,7 +47,7 @@ const jsLoader: IBundlessLoader = function (content) { outputOpts.declaration = true; } - const ret = transformer.call( + const ret = transformer.fn!.call( { config: this.config, pkg: this.pkg, diff --git a/src/builder/bundless/loaders/javascript/swc.ts b/src/builder/bundless/loaders/javascript/swc.ts index c2acb6a1..0b86bbf6 100644 --- a/src/builder/bundless/loaders/javascript/swc.ts +++ b/src/builder/bundless/loaders/javascript/swc.ts @@ -8,7 +8,7 @@ import { getBundlessTargets, getSWCTransformReactOpts, } from '../../../utils'; -import { IJSTransformer } from '../types'; +import { IJSTransformerFn } from '../types'; const isTs = (p: string): boolean => p.endsWith('.ts') || p.endsWith('.tsx'); @@ -102,7 +102,7 @@ export const replaceAbsPathWithRelativePath = (opts: { /** * swc transformer */ -const swcTransformer: IJSTransformer = async function (content) { +const swcTransformer: IJSTransformerFn = async function (content) { // swc will install on demand, so should import dynamic const { transform }: typeof import('@swc/core') = require('@swc/core'); diff --git a/src/builder/bundless/loaders/types.ts b/src/builder/bundless/loaders/types.ts index e6c4acd7..87599ccc 100644 --- a/src/builder/bundless/loaders/types.ts +++ b/src/builder/bundless/loaders/types.ts @@ -31,7 +31,7 @@ export type IBundlessLoader = ( this: Omit & ILoaderContext & { cwd: string; - + transformers: Record; itemDistAbsPath: string; /** @@ -55,7 +55,7 @@ type IJSTransformerResult = [ILoaderOutput['content'], SourceMap?]; /** * bundless transformer type */ -export type IJSTransformer = ( +export type IJSTransformerFn = ( this: ILoaderContext & { paths: { cwd: string; @@ -65,3 +65,8 @@ export type IJSTransformer = ( }, content: Parameters[0], ) => IJSTransformerResult | Promise; + +export type IJSTransformer = { + fn?: IJSTransformerFn; + resolvePath: string; +}; diff --git a/src/builder/bundless/parallelLoader.ts b/src/builder/bundless/parallelLoader.ts new file mode 100644 index 00000000..e946a79d --- /dev/null +++ b/src/builder/bundless/parallelLoader.ts @@ -0,0 +1,23 @@ +import path from 'path'; +import { Piscina } from 'piscina'; +import type { IBundlessConfig } from 'src/builder/config'; +import { IApi } from 'src/types'; +import type { Loaders, Transformers } from '.'; + +export default () => + new Piscina<{ + fileAbsPath: string; + loaders: Loaders; + fileDistPath: string; + transformers: Transformers; + opts: { + config: IBundlessConfig; + pkg: IApi['pkg']; + cwd: string; + itemDistAbsPath: string; + }; + }>({ + filename: path.resolve(__dirname + '/loaders/index.js'), + idleTimeout: 30000, + recordTiming: false, + }); diff --git a/src/builder/config.ts b/src/builder/config.ts index 5cddea80..38bb47d0 100644 --- a/src/builder/config.ts +++ b/src/builder/config.ts @@ -40,6 +40,7 @@ export interface IBundlessConfig type: IFatherBuildTypes.BUNDLESS; format: IFatherBundlessTypes; input: string; + parallel: boolean; output: NonNullable; } @@ -177,6 +178,7 @@ export function normalizeUserConfig( type: IFatherBuildTypes.BUNDLESS, format: formatName as IFatherBundlessTypes, platform: userConfig.platform || defaultPlatform, + parallel: false, ...baseConfig, ...esmBaseConfig, }; diff --git a/src/features/configBuilder/configBuilder.ts b/src/features/configBuilder/configBuilder.ts index 009aa364..2a9a727d 100644 --- a/src/features/configBuilder/configBuilder.ts +++ b/src/features/configBuilder/configBuilder.ts @@ -1,16 +1,14 @@ import { + ITransformerItem, + Loaders, addLoader as addBundlessLoader, - ILoaderItem, -} from '../../builder/bundless/loaders'; -import { addTransformer as addJSTransformer, - ITransformerItem, -} from '../../builder/bundless/loaders/javascript'; +} from '../../builder/bundless'; import { IApi, IFatherJSTransformerTypes } from '../../types'; export default async (api: IApi) => { // collect all bundless loaders - const bundlessLoaders: ILoaderItem[] = await api.applyPlugins({ + const bundlessLoaders: Loaders = await api.applyPlugins({ key: 'addBundlessLoader', initialValue: [ { diff --git a/src/features/configPlugins/schema.ts b/src/features/configPlugins/schema.ts index 3e577509..dc19d91b 100644 --- a/src/features/configPlugins/schema.ts +++ b/src/features/configPlugins/schema.ts @@ -40,6 +40,7 @@ function getBundlessSchemas(Joi: Root) { IFatherJSTransformerTypes.ESBUILD, IFatherJSTransformerTypes.SWC, ).optional(), + parallel: Joi.bool(), overrides: Joi.object(), ignores: Joi.array().items(Joi.string()), }); diff --git a/src/types.ts b/src/types.ts index de9930a4..8d8f2557 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,9 +5,9 @@ import type { IConfig as IBundlerWebpackConfig } from '@umijs/bundler-webpack/di import type { IAdd, IModify, IServicePluginAPI, PluginAPI } from '@umijs/core'; import type { ITransformerItem } from './builder/bundless/loaders/javascript'; import type { - createConfigProviders, IBundleConfig, IBundlessConfig, + createConfigProviders, } from './builder/config'; import type { IDoctorReport } from './doctor'; import type { IDoctorSourceParseResult } from './doctor/parser'; @@ -159,6 +159,11 @@ export interface IFatherBundlessConfig extends IFatherBaseConfig { * ignore specific directories & files via ignore syntax */ ignores?: string[]; + + /** + * compiler parallel + */ + parallel?: boolean; } export interface IFatherBundleConfig extends IFatherBaseConfig { diff --git a/tests/parallel.test.ts b/tests/parallel.test.ts new file mode 100644 index 00000000..b1bc648c --- /dev/null +++ b/tests/parallel.test.ts @@ -0,0 +1,68 @@ +import path from 'path'; +import * as cli from '../dist/cli/cli'; +import { distToMap, getDirCases } from './utils'; + +global.CASES_DIR = path.join(__dirname, 'fixtures/build'); + +const setupRcFileMocks = (cases, casesDir) => { + cases.forEach((name) => { + const rcFilePath = path.join(casesDir, name, '.fatherrc.ts'); + jest.doMock(rcFilePath, () => { + const originalModule = jest.requireActual(rcFilePath); + console.log(originalModule.default, 'originalModule'); + originalModule.default.esm && + (originalModule.default.esm = { + ...originalModule.default.esm, + parallel: true, + }); + originalModule.default.cjs && + (originalModule.default.cjs = { + ...originalModule.default.cjs, + parallel: true, + }); + return { + __esModule: true, + default: originalModule.default, + }; + }); + }); +}; + +const uninstallRcFileMocks = (cases, casesDir) => { + cases.forEach((name) => { + const rcFilePath = path.join(casesDir, name, '.fatherrc.ts'); + jest.unmock(rcFilePath); + }); +}; + +beforeAll(() => { + process.env.FATHER_CACHE = 'none'; + setupRcFileMocks(cases, global.CASES_DIR); +}); + +afterAll(() => { + uninstallRcFileMocks(cases, global.CASES_DIR); + delete process.env.APP_ROOT; + delete process.env.FATHER_CACHE; +}); + +// generate cases +const cases = getDirCases(global.CASES_DIR).filter((item) => + item.includes('bundless'), +); + +for (let name of cases) { + test(`parllel build: ${name}`, async () => { + // execute build + process.env.APP_ROOT = path.join(global.CASES_DIR, name); + await cli.run({ + args: { _: ['build'], $0: 'node' }, + }); + + // prepare file map + const fileMap = distToMap(path.join(global.CASES_DIR, name, 'dist')); + + // check result + require(`${global.CASES_DIR}/${name}/expect`).default(fileMap); + }); +}