From cbf76406e044546494f30033625d5c15efc51ace Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Mon, 22 Feb 2021 14:37:14 +0800 Subject: [PATCH 1/7] feat(wip): add filesystem cache config for webpack 5 New: - Turn on `filsystem` cache by default in webpack 5 - An `addBuildDependencies` plugin API for plugins to add additional dependencies to the cache config To Fix: - Need to implement a `--no-cache` option for both `serve` and `build` command - Need to decide how to deal with `.js` config files when a random environment variable change may affect its exports - `lintOnSave` option needs an overhaul to work better with webpack 5 and the new cache mechanism - Not sure how to test the cache implementation --- packages/@vue/cli-service/lib/PluginAPI.js | 22 +++++++++++++++++++ packages/@vue/cli-service/lib/Service.js | 12 +++++++--- packages/@vue/cli-service/lib/config/base.js | 18 ++++++++++++++- packages/@vue/cli-service/lib/options.js | 6 +++++ .../cli-service/types/ProjectOptions.d.ts | 6 +++++ 5 files changed, 60 insertions(+), 4 deletions(-) diff --git a/packages/@vue/cli-service/lib/PluginAPI.js b/packages/@vue/cli-service/lib/PluginAPI.js index 88db286f40..e2aedbb7ed 100644 --- a/packages/@vue/cli-service/lib/PluginAPI.js +++ b/packages/@vue/cli-service/lib/PluginAPI.js @@ -212,6 +212,28 @@ class PluginAPI { const cacheIdentifier = hash(variables) return { cacheDirectory, cacheIdentifier } } + + /** + * Vue CLI enables the filesystem cache in webpack 5 by default. + * If your plugin depends on an additional config file (e.g. `stylelint.config.js`), + * the default cache config does not take that file into account, + * thus causing unexpected cache persistence. + * To avoid such problems, you should add that file to the `buildDependencies` + * of webpack by calling this function. + * (e.g. `api.addBuildDependencies('stylelint.config.js')`) + * @param {string[]|Object} deps if an array of strings is passed, it will be + * appended to the cache.buildDependencies.config field; if an object is passed, + * it will be merged into the cache.buildDependencies filed + */ + addBuildDependencies (deps, { shouldEvaluate }) { + if (Array.isArray(deps)) { + this.configureWebpack(() => ({ cache: { buildDependencies: { config: deps } } })) + } else { + this.configureWebpack(() => ({ cache: { buildDependencies: deps } })) + } + + // FIXME: how to deal with random environment variables in `.js` buildDependencies? + } } module.exports = PluginAPI diff --git a/packages/@vue/cli-service/lib/Service.js b/packages/@vue/cli-service/lib/Service.js index 8f0bd289bc..ed892e0b8a 100644 --- a/packages/@vue/cli-service/lib/Service.js +++ b/packages/@vue/cli-service/lib/Service.js @@ -349,11 +349,9 @@ module.exports = class Service { } if (!fileConfig || typeof fileConfig !== 'object') { - // TODO: show throw an Error here, to be fixed in v5 - error( + throw new Error( `Error loading ${chalk.bold(fileConfigPath)}: should export an object or a function that returns object.` ) - fileConfig = null } } catch (e) { error(`Error loading ${chalk.bold(fileConfigPath)}:`) @@ -384,12 +382,20 @@ module.exports = class Service { } resolved = fileConfig resolvedFrom = 'vue.config.js' + + // add the path to buildDependencies + this.plugins.push({ + id: 'set-build-dependencies', + apply: (api) => api.addBuildDependencies([fileConfigPath]) + }) } else if (pkgConfig) { resolved = pkgConfig resolvedFrom = '"vue" field in package.json' } else { resolved = this.inlineOptions || {} resolvedFrom = 'inline options' + + // FIXME: may need to add inline options hash to cache name } if (resolved.css && typeof resolved.css.modules !== 'undefined') { diff --git a/packages/@vue/cli-service/lib/config/base.js b/packages/@vue/cli-service/lib/config/base.js index f821c8fedb..60a9d5355a 100644 --- a/packages/@vue/cli-service/lib/config/base.js +++ b/packages/@vue/cli-service/lib/config/base.js @@ -12,12 +12,28 @@ module.exports = (api, options) => { const isLegacyBundle = process.env.VUE_CLI_MODERN_MODE && !process.env.VUE_CLI_MODERN_BUILD const resolveLocal = require('../util/resolveLocal') - // https://github.com/webpack/webpack/issues/11467#issuecomment-691873586 if (webpackMajor !== 4) { + // https://github.com/webpack/webpack/issues/11467#issuecomment-691873586 webpackConfig.module .rule('esm') .test(/\.m?jsx?$/) .resolve.set('fullySpecified', false) + + // Filesystem cache only available for webpack 5 + webpackConfig.cache({ + type: 'filesystem', + // Set different cache name for different modes and targets (and commands). + // Since we use environment variables for all kinds of purposes, + // the most straightforward way is to just find and combine those + // Vue CLI-specific environment variables. + name: Object.entries(process.env) + .filter(([key]) => key.startsWith('VUE_CLI')) + .map(([key, value]) => `${key}-${value}`) + .join(';'), + buildDependencies: { + config: [require.resolve('../../webpack.config')] + } + }) } webpackConfig diff --git a/packages/@vue/cli-service/lib/options.js b/packages/@vue/cli-service/lib/options.js index 661b9a8684..c3c93d6373 100644 --- a/packages/@vue/cli-service/lib/options.js +++ b/packages/@vue/cli-service/lib/options.js @@ -55,6 +55,12 @@ const schema = createSchema(joi => joi.object({ }) }), + // for cache invalidation + buildDependencies: joi.alternatives().try( + joi.array().items(joi.string().required()), + joi.object() + ), + // webpack chainWebpack: joi.func(), configureWebpack: joi.alternatives().try( diff --git a/packages/@vue/cli-service/types/ProjectOptions.d.ts b/packages/@vue/cli-service/types/ProjectOptions.d.ts index 0485ba5c0b..9890f56e47 100644 --- a/packages/@vue/cli-service/types/ProjectOptions.d.ts +++ b/packages/@vue/cli-service/types/ProjectOptions.d.ts @@ -131,6 +131,12 @@ interface ProjectOptions { css?: CSSOptions; + /** + * Tell webpack to invalidate the build cache when changes are detected in these additional buildDependencies. + * Can be an array of config file paths or an object that is accepted by the webpack `cache.buildDependencies` option. + */ + buildDependencies?: Array | Object, + /** * A function that will receive an instance of `ChainableConfig` powered by [webpack-chain](https://github.com/mozilla-neutrino/webpack-chain) */ From 57cb6769eef7ef63513d9906e0090ab5e0436abb Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Tue, 2 Mar 2021 14:29:50 +0800 Subject: [PATCH 2/7] fix: add NODE_ENV to cache name --- packages/@vue/cli-service/lib/config/base.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@vue/cli-service/lib/config/base.js b/packages/@vue/cli-service/lib/config/base.js index 60a9d5355a..faeb753c91 100644 --- a/packages/@vue/cli-service/lib/config/base.js +++ b/packages/@vue/cli-service/lib/config/base.js @@ -27,7 +27,7 @@ module.exports = (api, options) => { // the most straightforward way is to just find and combine those // Vue CLI-specific environment variables. name: Object.entries(process.env) - .filter(([key]) => key.startsWith('VUE_CLI')) + .filter(([key]) => key.startsWith('VUE_CLI') || key === 'NODE_ENV') .map(([key, value]) => `${key}-${value}`) .join(';'), buildDependencies: { From fefc5356320a1e76fe9bfb79506455d5f308572e Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Thu, 4 Mar 2021 14:53:17 +0800 Subject: [PATCH 3/7] chore: maybe we need an `appendCacheIdentifier` API? --- packages/@vue/cli-service/lib/PluginAPI.js | 13 ++++++++++--- packages/@vue/cli-service/lib/Service.js | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/@vue/cli-service/lib/PluginAPI.js b/packages/@vue/cli-service/lib/PluginAPI.js index e2aedbb7ed..c09c35f501 100644 --- a/packages/@vue/cli-service/lib/PluginAPI.js +++ b/packages/@vue/cli-service/lib/PluginAPI.js @@ -218,22 +218,29 @@ class PluginAPI { * If your plugin depends on an additional config file (e.g. `stylelint.config.js`), * the default cache config does not take that file into account, * thus causing unexpected cache persistence. + * * To avoid such problems, you should add that file to the `buildDependencies` * of webpack by calling this function. * (e.g. `api.addBuildDependencies('stylelint.config.js')`) + * + * Please be aware that these dependencies are not evaluated. + * So if you depend on some external environment variables, please + * use the `appendCacheIdentifier` API to explicitly monitor their changes. + * * @param {string[]|Object} deps if an array of strings is passed, it will be * appended to the cache.buildDependencies.config field; if an object is passed, * it will be merged into the cache.buildDependencies filed */ - addBuildDependencies (deps, { shouldEvaluate }) { + addBuildDependencies (deps) { if (Array.isArray(deps)) { this.configureWebpack(() => ({ cache: { buildDependencies: { config: deps } } })) } else { this.configureWebpack(() => ({ cache: { buildDependencies: deps } })) } - - // FIXME: how to deal with random environment variables in `.js` buildDependencies? } + + // FIXME: Deal with random environment variables in `.js` buildDependencies + appendCacheIdentifier () {} } module.exports = PluginAPI diff --git a/packages/@vue/cli-service/lib/Service.js b/packages/@vue/cli-service/lib/Service.js index ed892e0b8a..94906c491b 100644 --- a/packages/@vue/cli-service/lib/Service.js +++ b/packages/@vue/cli-service/lib/Service.js @@ -395,7 +395,7 @@ module.exports = class Service { resolved = this.inlineOptions || {} resolvedFrom = 'inline options' - // FIXME: may need to add inline options hash to cache name + // FIXME: may need to call `api.appendCacheIdentifier` with inline options hash } if (resolved.css && typeof resolved.css.modules !== 'undefined') { From 0a3983a8a6fb19a1135abe1cbe2f7fb1e6433a15 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Fri, 4 Jun 2021 17:38:41 +0800 Subject: [PATCH 4/7] test: list things we need to validate before shipping the cache feature --- .../@vue/cli-service/__tests__/cache.spec.js | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 packages/@vue/cli-service/__tests__/cache.spec.js diff --git a/packages/@vue/cli-service/__tests__/cache.spec.js b/packages/@vue/cli-service/__tests__/cache.spec.js new file mode 100644 index 0000000000..af77a33f7a --- /dev/null +++ b/packages/@vue/cli-service/__tests__/cache.spec.js @@ -0,0 +1,21 @@ +test.todo('should ignore cache when `--no-cache` argument is passed') + +test.todo('should ignore previous cache when `--mode` argument changed') + +test.todo('should ignore previous cache when corresponding `.env` file changed') + +test.todo('should not ignore previous cache when irrelevant `.env` file changed') + +test.todo('should ignore previous cache when `--target` argument changed') + +test.todo('should ignore previous cache when `--module` flag is changed') + +test.todo('should ignore previous cache when `package.json` changed') + +test.todo('should ignore previous cache when lockfiles changed') + +test.todo('should ignore previous cache when Vue CLI config file changed') + +test.todo('should ignore previous cache when babel config changed') + +test.todo('should ignore previous cache when tsconfig.json changed') From 411450ccf692e4284cfebb1d5c9218592a10ca67 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Fri, 4 Jun 2021 17:41:44 +0800 Subject: [PATCH 5/7] test: eslint cache should be tested too --- packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js index ea43a06ac0..b4fefbdd68 100644 --- a/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js +++ b/packages/@vue/cli-plugin-eslint/__tests__/eslintPlugin.spec.js @@ -269,3 +269,5 @@ test(`should use formatter 'codeframe'`, async () => { await donePromise }) + +test.todo('should not skip lint when `vue-cli-service serve` hits the filesystem cache') From 6acc5a9c2e56a020f86f8014326ad8bd486a8207 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Mon, 7 Jun 2021 13:59:31 +0800 Subject: [PATCH 6/7] test: should test different commands for the `--no-cache` argument --- packages/@vue/cli-service/__tests__/cache.spec.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/@vue/cli-service/__tests__/cache.spec.js b/packages/@vue/cli-service/__tests__/cache.spec.js index af77a33f7a..7a6870a795 100644 --- a/packages/@vue/cli-service/__tests__/cache.spec.js +++ b/packages/@vue/cli-service/__tests__/cache.spec.js @@ -1,4 +1,8 @@ -test.todo('should ignore cache when `--no-cache` argument is passed') +test.todo('should ignore serve cache when `--no-cache` argument is passed') + +test.todo('should ignore build cache when `--no-cache` argument is passed') + +test.todo('should ignore test cache when `--no-cache` argument is passed') test.todo('should ignore previous cache when `--mode` argument changed') From e3cfcf57a30bb58ccc7895fe1d3242fcef797501 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Mon, 7 Jun 2021 15:43:12 +0800 Subject: [PATCH 7/7] test: add two simple tests for filesystem cache --- .../@vue/cli-service/__tests__/cache.spec.js | 38 ++++++++++++++++++- packages/@vue/cli-service/package.json | 1 + yarn.lock | 13 ++++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/packages/@vue/cli-service/__tests__/cache.spec.js b/packages/@vue/cli-service/__tests__/cache.spec.js index 7a6870a795..5dce852daa 100644 --- a/packages/@vue/cli-service/__tests__/cache.spec.js +++ b/packages/@vue/cli-service/__tests__/cache.spec.js @@ -1,10 +1,46 @@ +jest.setTimeout(30000) + +const path = require('path') + +const { defaultPreset } = require('@vue/cli/lib/options') +const create = require('@vue/cli-test-utils/createTestProject') +// const launchPuppeteer = require('@vue/cli-test-utils/launchPuppeteer') + +const { hashElement } = require('folder-hash') + +test('should build with cache on two consecutive runs', async () => { + const project = await create('cache-basic', defaultPreset) + const cacheDir = path.join(project.dir, './node_modules/.cache') + + await project.run('vue-cli-service build') + const cacheHash = await hashElement(cacheDir) + + await project.run('vue-cli-service build') + const newCacheHash = await hashElement(cacheDir) + + expect(newCacheHash).toEqual(cacheHash) + + // TODO: the mtime of the files in each of the sub directories should not change +}) + test.todo('should ignore serve cache when `--no-cache` argument is passed') test.todo('should ignore build cache when `--no-cache` argument is passed') test.todo('should ignore test cache when `--no-cache` argument is passed') -test.todo('should ignore previous cache when `--mode` argument changed') +test('should ignore previous cache when `--mode` argument changed', async () => { + const project = await create('cache-mode', defaultPreset) + const cacheDir = path.join(project.dir, './node_modules/.cache') + + await project.run('vue-cli-service build') + const cacheHash = await hashElement(cacheDir) + + await project.run('vue-cli-service build --mode development') + const newCacheHash = await hashElement(cacheDir) + + expect(newCacheHash).not.toEqual(cacheHash) +}) test.todo('should ignore previous cache when corresponding `.env` file changed') diff --git a/packages/@vue/cli-service/package.json b/packages/@vue/cli-service/package.json index 8b58987a28..130b1fef66 100644 --- a/packages/@vue/cli-service/package.json +++ b/packages/@vue/cli-service/package.json @@ -114,6 +114,7 @@ }, "devDependencies": { "fibers": ">= 3.1.1 <6.0.0", + "folder-hash": "^4.0.1", "sass": "^1.32.7", "sass-loader": "^11.0.1", "stylus-loader": "^5.0.0", diff --git a/yarn.lock b/yarn.lock index 468c199bab..24bfe45653 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10834,6 +10834,15 @@ focus-visible@^5.2.0: resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3" integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ== +folder-hash@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/folder-hash/-/folder-hash-4.0.1.tgz#1603c46bdb899843e292ecdfca9e540dd22e1236" + integrity sha512-oF1MGtGAPezYJJRMRPzTwtDYwZdQ16UTnthsVAxjVZnlrQ36WuF6YxSgyZxnoUEK6JNPX+04FCFAkw5CzE5OMw== + dependencies: + debug "^4.1.1" + graceful-fs "~4.2.0" + minimatch "~3.0.4" + follow-redirects@^1.0.0, follow-redirects@^1.10.0: version "1.14.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.0.tgz#f5d260f95c5f8c105894491feee5dc8993b402fe" @@ -11654,7 +11663,7 @@ graceful-fs@4.1.15: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== -graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: +graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@~4.2.0: version "4.2.6" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== @@ -15474,7 +15483,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= -"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: +"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2, minimatch@~3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==