diff --git a/package.json b/package.json index ecbe43b..4ac7e0e 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "babel-plugin-module-resolver": "4.1.0", "babel-plugin-react-require": "3.1.3", "babel-plugin-transform-define": "2.0.1", + "file-system-cache": "2.0.0", "loader-runner": "4.2.0", "minimatch": "3.1.2", "tsconfig-paths": "4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6018ac..f91164f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,7 @@ importers: babel-plugin-react-require: 3.1.3 babel-plugin-transform-define: 2.0.1 esno: ^0.16.3 + file-system-cache: 2.0.0 git-repo-info: ^2.1.1 husky: ^8.0.1 jest: ^27 @@ -50,6 +51,7 @@ importers: babel-plugin-module-resolver: 4.1.0 babel-plugin-react-require: 3.1.3 babel-plugin-transform-define: 2.0.1 + file-system-cache: 2.0.0 loader-runner: 4.2.0 minimatch: 3.1.2 tsconfig-paths: 4.0.0 @@ -3669,6 +3671,13 @@ packages: bser: 2.1.1 dev: true + /file-system-cache/2.0.0: + resolution: {integrity: sha512-QlYut2ZtxRgdW/dboSmiKZWM8FsnpLaLI549hN/RWgwn3FawSil7Jc2n7nFHheclvYxa4LJqwEOvNUYv9VsCXg==} + dependencies: + fs-extra: 10.1.0 + ramda: 0.28.0 + dev: false + /fill-range/4.0.0: resolution: {integrity: sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==} engines: {node: '>=0.10.0'} @@ -3751,7 +3760,6 @@ packages: graceful-fs: 4.2.10 jsonfile: 6.1.0 universalify: 2.0.0 - dev: true /fs-extra/7.0.1: resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} @@ -5024,7 +5032,6 @@ packages: universalify: 2.0.0 optionalDependencies: graceful-fs: 4.2.10 - dev: true /jsonparse/1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} @@ -6332,6 +6339,10 @@ packages: engines: {node: '>=8'} dev: true + /ramda/0.28.0: + resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==} + dev: false + /randombytes/2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -7383,7 +7394,6 @@ packages: /universalify/2.0.0: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} - dev: true /unset-value/1.0.0: resolution: {integrity: sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==} diff --git a/src/builder/bundless/dts/index.ts b/src/builder/bundless/dts/index.ts index acff3da..d4b5581 100644 --- a/src/builder/bundless/dts/index.ts +++ b/src/builder/bundless/dts/index.ts @@ -1,7 +1,9 @@ import { chalk, logger, winPath } from '@umijs/utils'; +import fs from 'fs'; import path from 'path'; // @ts-ignore import tsPathsTransformer from '../../../../compiled/@zerollup/ts-transform-paths'; +import { getCache } from '../../../utils'; /** * get parsed tsconfig.json for specific path @@ -29,6 +31,7 @@ export default async function getDeclarations( inputFiles: string[], opts: { cwd: string }, ) { + const cache = getCache('dts'); const output: { file: string; content: string; sourceFile: string }[] = []; // use require() rather than import(), to avoid jest runner to fail // ref: https://github.com/nodejs/node/issues/35889 @@ -71,6 +74,19 @@ export default async function getDeclarations( }); const tsHost = ts.createCompilerHost(tsconfig.options); + const cacheKeys = inputFiles.reduce>( + (ret, file) => ({ + ...ret, + // format: {path:mtime:config} + [file]: [ + file, + fs.lstatSync(file).mtimeMs, + JSON.stringify(tsconfig.options), + ].join(':'), + }), + {}, + ); + const cacheRets: Record = {}; tsHost.writeFile = (fileName, declaration, _a, _b, sourceFiles) => { const sourceFile = sourceFiles![0].fileName; @@ -78,14 +94,29 @@ export default async function getDeclarations( // only collect dts for input files, to avoid output error in watch mode // ref: https://github.com/umijs/father-next/issues/43 if (inputFiles.includes(sourceFile)) { - output.push({ + const ret = { file: path.basename(fileName), content: declaration, sourceFile, - }); + }; + + output.push(ret); + + // group cache by file (d.ts & d.ts.map) + cacheRets[cacheKeys[sourceFile]] ??= []; + cacheRets[cacheKeys[sourceFile]].push(ret); } }; + // use cache first + inputFiles = inputFiles.filter((file) => { + const cacheRet = cache.getSync(cacheKeys[file], ''); + + if (!cacheRet) return true; + output.push(...cacheRet); + return false; + }); + const program = ts.createProgram( inputFiles, tsconfig.options as any, @@ -111,6 +142,16 @@ export default async function getDeclarations( )}`, ); } + + // save cache + Object.keys(cacheRets).forEach((key) => cache.setSync(key, cacheRets[key])); + + // process no d.ts inputs, fallback to empty array + inputFiles.forEach((file) => { + const cacheKey = cacheKeys[file]; + + if (!cacheRets[cacheKey]) cache.setSync(cacheKey, []); + }); } return output; diff --git a/src/builder/bundless/loaders/index.ts b/src/builder/bundless/loaders/index.ts index 3e41fcc..2c14c2b 100644 --- a/src/builder/bundless/loaders/index.ts +++ b/src/builder/bundless/loaders/index.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import { runLoaders } from 'loader-runner'; import type { IApi } from '../../../types'; +import { getCache } from '../../../utils'; import type { IBundlessConfig } from '../../config'; import type { IBundlessLoader, ILoaderOutput } from './types'; @@ -42,6 +43,18 @@ export default async ( fileAbsPath: string, opts: { config: IBundlessConfig; pkg: IApi['pkg']; cwd: string }, ) => { + const cache = getCache('loader'); + // format: {path:mtime:config} + const cacheKey = [ + fileAbsPath, + fs.statSync(fileAbsPath).mtimeMs, + JSON.stringify(opts.config), + ].join(':'); + const cacheRet = await cache.get(cacheKey, ''); + + // use cache first + if (cacheRet) return Promise.resolve(cacheRet); + // get matched loader by test const matched = loaders.find((item) => { switch (typeof item.test) { @@ -81,10 +94,14 @@ export default async ( reject(err); } else if (result) { // FIXME: handle buffer type? - resolve({ + const ret = { content: result[0] as unknown as string, options: outputOpts, - }); + }; + + // save cache then resolve + cache.set(cacheKey, ret); + resolve(ret); } else { resolve(void 0); } diff --git a/src/constants.ts b/src/constants.ts index 25191b9..24c9887 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,3 +5,4 @@ export const WATCH_DEBOUNCE_STEP = 300; export const DEV_COMMAND = 'dev'; export const BUILD_COMMANDS = ['build', 'prebundle']; export const DEBUG_BUNDLESS_NAME = 'father:bundless'; +export const CACHE_PATH = 'node_modules/.cache/father'; diff --git a/src/utils.ts b/src/utils.ts index 068a3ef..b265bde 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,22 @@ import { pkgUp } from '@umijs/utils'; +import Cache from 'file-system-cache'; import path from 'path'; +import { CACHE_PATH } from './constants'; import { IApi } from './types'; +const caches: Record> = {}; + +/** + * get file-system cache for specific namespace + */ +export function getCache(ns: string): typeof caches['0'] { + // return fake cache if cache disabled + if (process.env.FATHER_CACHE === 'none') { + return { set() {}, get() {}, setSync() {}, getSync() {} } as any; + } + return (caches[ns] ??= Cache({ basePath: CACHE_PATH, ns })); +} + /** * get valid type field value from package.json */ diff --git a/tests/build.test.ts b/tests/build.test.ts index b448aec..bec0501 100644 --- a/tests/build.test.ts +++ b/tests/build.test.ts @@ -1,11 +1,16 @@ import path from 'path'; -import { distToMap, getDirCases } from './utils'; import * as cli from '../src/cli/cli'; +import { distToMap, getDirCases } from './utils'; const CASES_DIR = path.join(__dirname, 'fixtures/build'); +beforeAll(() => { + process.env.FATHER_CACHE = 'none'; +}); + afterAll(() => { delete process.env.APP_ROOT; + delete process.env.FATHER_CACHE; }); // generate cases diff --git a/tests/config.test.ts b/tests/config.test.ts index 684bea1..2b19f11 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -1,7 +1,7 @@ -import path from 'path'; import { mockProcessExit } from 'jest-mock-process'; -import { distToMap } from './utils'; +import path from 'path'; import * as cli from '../src/cli/cli'; +import { distToMap } from './utils'; jest.mock('@umijs/utils', () => { const originalModule = jest.requireActual('@umijs/utils'); @@ -25,8 +25,13 @@ jest.mock('@umijs/utils', () => { const mockExit = mockProcessExit(); const CASES_DIR = path.join(__dirname, 'fixtures/config'); +beforeAll(() => { + process.env.FATHER_CACHE = 'none'; +}); + afterAll(() => { delete process.env.APP_ROOT; + delete process.env.FATHER_CACHE; mockExit.mockRestore(); }); test('config: cyclic extends', async () => {