From d8877b51f73da3ee177b09eece02539b8b345ddc Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 7 Apr 2021 16:45:02 +0800 Subject: [PATCH 1/6] feat: support `vue.config.mjs` In Node.js 12.17+ only. Otherwise, the `--experimental-modules` flag need to be turned on for Node. Not compatible with `eslint-import-resolver-webpack`. --- .../cli-service/__tests__/ServiceESM.spec.js | 75 ++++++---- packages/@vue/cli-service/lib/Service.js | 137 ++++-------------- .../@vue/cli-service/lib/util/checkWebpack.js | 1 + .../cli-service/lib/util/loadFileConfig.js | 38 +++++ .../cli-service/lib/util/resolveUserConfig.js | 81 +++++++++++ packages/@vue/cli-service/package.json | 1 + yarn.lock | 7 + 7 files changed, 201 insertions(+), 139 deletions(-) create mode 100644 packages/@vue/cli-service/lib/util/loadFileConfig.js create mode 100644 packages/@vue/cli-service/lib/util/resolveUserConfig.js diff --git a/packages/@vue/cli-service/__tests__/ServiceESM.spec.js b/packages/@vue/cli-service/__tests__/ServiceESM.spec.js index de2789108e..5bb0546d43 100644 --- a/packages/@vue/cli-service/__tests__/ServiceESM.spec.js +++ b/packages/@vue/cli-service/__tests__/ServiceESM.spec.js @@ -1,52 +1,63 @@ -const Service = require('../lib/Service') - const path = require('path') -const configPath = path.resolve('/', 'vue.config.cjs') - -jest.mock('fs') -const fs = require('fs') - -beforeEach(() => { - fs.writeFileSync(path.resolve('/', 'package.json'), JSON.stringify({ - type: 'module', - vue: { - lintOnSave: 'default' - } - }, null, 2)) -}) - -afterEach(() => { - if (fs.existsSync(configPath)) { - fs.unlinkSync(configPath) - } +const fs = require('fs-extra') + +const { defaultPreset } = require('@vue/cli/lib/options') +const create = require('@vue/cli-test-utils/createTestProject') +const { loadModule } = require('@vue/cli-shared-utils') + +let project +beforeAll(async () => { + project = await create('service-esm-test', defaultPreset) + const pkg = JSON.parse(await project.read('package.json')) + pkg.type = 'module' + pkg.vue = { lintOnSave: 'default' } + await project.write('package.json', JSON.stringify(pkg, null, 2)) + fs.renameSync(path.resolve(project.dir, 'babel.config.js'), path.resolve(project.dir, 'babel.config.cjs')) }) -const createService = () => { - const service = new Service('/', { +const createService = async () => { + const Service = loadModule('@vue/cli-service/lib/Service', project.dir) + const service = new Service(project.dir, { plugins: [], useBuiltIn: false }) - service.init() + await service.init() return service } -// vue.config.cjs has higher priority - test('load project options from package.json', async () => { - const service = createService() + const service = await createService() expect(service.projectOptions.lintOnSave).toBe('default') }) test('load project options from vue.config.cjs', async () => { - fs.writeFileSync(configPath, '') - jest.mock(configPath, () => ({ lintOnSave: true }), { virtual: true }) - const service = createService() + const configPath = path.resolve(project.dir, './vue.config.cjs') + fs.writeFileSync(configPath, 'module.exports = { lintOnSave: true }') + const service = await createService() expect(service.projectOptions.lintOnSave).toBe(true) + await fs.unlinkSync(configPath) }) test('load project options from vue.config.cjs as a function', async () => { - fs.writeFileSync(configPath, '') - jest.mock(configPath, () => function () { return { lintOnSave: true } }, { virtual: true }) - const service = createService() + const configPath = path.resolve(project.dir, './vue.config.cjs') + fs.writeFileSync(configPath, 'module.exports = function () { return { lintOnSave: true } }') + const service = await createService() + expect(service.projectOptions.lintOnSave).toBe(true) + await fs.unlinkSync(configPath) +}) + +test('load project options from vue.config.js', async () => { + const configPath = path.resolve(project.dir, './vue.config.js') + fs.writeFileSync(configPath, 'export default { lintOnSave: true }') + const service = await createService() + expect(service.projectOptions.lintOnSave).toBe(true) + await fs.unlinkSync(configPath) +}) + +test('load project options from vue.config.mjs', async () => { + const configPath = path.resolve(project.dir, './vue.config.mjs') + fs.writeFileSync(configPath, 'export default { lintOnSave: true }') + const service = await createService() expect(service.projectOptions.lintOnSave).toBe(true) + await fs.unlinkSync(configPath) }) diff --git a/packages/@vue/cli-service/lib/Service.js b/packages/@vue/cli-service/lib/Service.js index 032f7069a5..da8e826859 100644 --- a/packages/@vue/cli-service/lib/Service.js +++ b/packages/@vue/cli-service/lib/Service.js @@ -1,4 +1,3 @@ -const fs = require('fs') const path = require('path') const debug = require('debug') const { merge } = require('webpack-merge') @@ -7,10 +6,12 @@ const PluginAPI = require('./PluginAPI') const dotenv = require('dotenv') const dotenvExpand = require('dotenv-expand') const defaultsDeep = require('lodash.defaultsdeep') -const { chalk, warn, error, isPlugin, resolvePluginId, loadModule, resolvePkg, resolveModule } = require('@vue/cli-shared-utils') +const { warn, error, isPlugin, resolvePluginId, loadModule, resolvePkg, resolveModule } = require('@vue/cli-shared-utils') -const { defaults, validate } = require('./options') -const checkWebpack = require('@vue/cli-service/lib/util/checkWebpack') +const { defaults } = require('./options') +const checkWebpack = require('./util/checkWebpack') +const loadFileConfig = require('./util/loadFileConfig') +const resolveUserConfig = require('./util/resolveUserConfig') module.exports = class Service { constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) { @@ -55,7 +56,7 @@ module.exports = class Service { return pkg } - init (mode = process.env.VUE_CLI_MODE) { + async init (mode = process.env.VUE_CLI_MODE) { if (this.initialized) { return } @@ -70,7 +71,7 @@ module.exports = class Service { this.loadEnv() // load user config - const userOptions = this.loadUserOptions() + const userOptions = await this.loadUserOptions() this.projectOptions = defaultsDeep(userOptions, defaults()) debug('vue:project-config')(this.projectOptions) @@ -227,7 +228,7 @@ module.exports = class Service { this.setPluginsToSkip(args) // load env variables, load user config, apply plugins - this.init(mode) + await this.init(mode) args._ = args._ || [] let command = this.commands[name] @@ -316,110 +317,32 @@ module.exports = class Service { return config } + // Note: we intentionally make this function synchronous by default + // because eslint-import-resolver-webpack does not support async webpack configs. loadUserOptions () { - // vue.config.c?js - let fileConfig, pkgConfig, resolved, resolvedFrom - const esm = this.pkg.type && this.pkg.type === 'module' - - const possibleConfigPaths = [ - process.env.VUE_CLI_SERVICE_CONFIG_PATH, - './vue.config.js', - './vue.config.cjs' - ] - - let fileConfigPath - for (const p of possibleConfigPaths) { - const resolvedPath = p && path.resolve(this.context, p) - if (resolvedPath && fs.existsSync(resolvedPath)) { - fileConfigPath = resolvedPath - break - } - } - - if (fileConfigPath) { - if (esm && fileConfigPath === './vue.config.js') { - throw new Error(`Please rename ${chalk.bold('vue.config.js')} to ${chalk.bold('vue.config.cjs')} when ECMAScript modules is enabled`) - } - - try { - fileConfig = loadModule(fileConfigPath, this.context) - - if (typeof fileConfig === 'function') { - fileConfig = fileConfig() - } - - if (!fileConfig || typeof fileConfig !== 'object') { - // TODO: show throw an Error here, to be fixed in v5 - 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)}:`) - throw e - } - } - - // package.vue - pkgConfig = this.pkg.vue - if (pkgConfig && typeof pkgConfig !== 'object') { - error( - `Error loading vue-cli config in ${chalk.bold(`package.json`)}: ` + - `the "vue" field should be an object.` - ) - pkgConfig = null - } - - if (fileConfig) { - if (pkgConfig) { - warn( - `"vue" field in package.json ignored ` + - `due to presence of ${chalk.bold('vue.config.js')}.` - ) - warn( - `You should migrate it into ${chalk.bold('vue.config.js')} ` + - `and remove it from package.json.` - ) - } - resolved = fileConfig - resolvedFrom = 'vue.config.js' - } else if (pkgConfig) { - resolved = pkgConfig - resolvedFrom = '"vue" field in package.json' - } else { - resolved = this.inlineOptions || {} - resolvedFrom = 'inline options' - } - - // normalize some options - ensureSlash(resolved, 'publicPath') - if (typeof resolved.publicPath === 'string') { - resolved.publicPath = resolved.publicPath.replace(/^\.\//, '') + const { fileConfig, fileConfigPath } = loadFileConfig(this.context) + + // Seems we can't use `instanceof Promise` here (would fail the tests) + if (fileConfig && typeof fileConfig.then === 'function') { + return fileConfig + .then(mod => { + // fs.writeFileSync(`${this.context}/aaaa`, `mod ${JSON.stringify(mod, null, 2)}`) + return mod.default + }) + .then(loadedConfig => resolveUserConfig({ + inlineOptions: this.inlineOptions, + pkgConfig: this.pkg.vue, + fileConfig: loadedConfig, + fileConfigPath + })) } - removeSlash(resolved, 'outputDir') - // validate options - validate(resolved, msg => { - error( - `Invalid options in ${chalk.bold(resolvedFrom)}: ${msg}` - ) + return resolveUserConfig({ + inlineOptions: this.inlineOptions, + pkgConfig: this.pkg.vue, + fileConfig, + fileConfigPath }) - - return resolved - } -} - -function ensureSlash (config, key) { - const val = config[key] - if (typeof val === 'string') { - config[key] = val.replace(/([^/])$/, '$1/') - } -} - -function removeSlash (config, key) { - if (typeof config[key] === 'string') { - config[key] = config[key].replace(/\/$/g, '') } } diff --git a/packages/@vue/cli-service/lib/util/checkWebpack.js b/packages/@vue/cli-service/lib/util/checkWebpack.js index 0078cf4ccf..f9d42eba4d 100644 --- a/packages/@vue/cli-service/lib/util/checkWebpack.js +++ b/packages/@vue/cli-service/lib/util/checkWebpack.js @@ -24,6 +24,7 @@ module.exports = function checkWebpack (cwd) { // Check the package.json, // and only load from the project if webpack is explictly depended on, // in case of accidental hoisting. + let pkg = {} try { pkg = loadModule('./package.json', cwd) diff --git a/packages/@vue/cli-service/lib/util/loadFileConfig.js b/packages/@vue/cli-service/lib/util/loadFileConfig.js new file mode 100644 index 0000000000..ab01117d94 --- /dev/null +++ b/packages/@vue/cli-service/lib/util/loadFileConfig.js @@ -0,0 +1,38 @@ +const fs = require('fs') +const path = require('path') + +const isFileEsm = require('is-file-esm') +const { loadModule } = require('@vue/cli-shared-utils') + +module.exports = function loadFileConfig (context) { + let fileConfig, fileConfigPath + + const possibleConfigPaths = [ + process.env.VUE_CLI_SERVICE_CONFIG_PATH, + './vue.config.js', + './vue.config.cjs', + './vue.config.mjs' + ] + for (const p of possibleConfigPaths) { + const resolvedPath = p && path.resolve(context, p) + if (resolvedPath && fs.existsSync(resolvedPath)) { + fileConfigPath = resolvedPath + break + } + } + + if (fileConfigPath) { + const { esm } = isFileEsm.sync(fileConfigPath) + + if (esm) { + fileConfig = import(fileConfigPath) + } else { + fileConfig = loadModule(fileConfigPath, context) + } + } + + return { + fileConfig, + fileConfigPath + } +} diff --git a/packages/@vue/cli-service/lib/util/resolveUserConfig.js b/packages/@vue/cli-service/lib/util/resolveUserConfig.js new file mode 100644 index 0000000000..6511742cdb --- /dev/null +++ b/packages/@vue/cli-service/lib/util/resolveUserConfig.js @@ -0,0 +1,81 @@ +const path = require('path') +const { chalk, warn, error } = require('@vue/cli-shared-utils') +const { validate } = require('../options') + +function ensureSlash (config, key) { + const val = config[key] + if (typeof val === 'string') { + config[key] = val.replace(/([^/])$/, '$1/') + } +} + +function removeSlash (config, key) { + if (typeof config[key] === 'string') { + config[key] = config[key].replace(/\/$/g, '') + } +} + +module.exports = function resolveUserConfig ({ + inlineOptions, + pkgConfig, + fileConfig, + fileConfigPath +}) { + if (fileConfig) { + if (typeof fileConfig === 'function') { + fileConfig = fileConfig() + } + + if (!fileConfig || typeof fileConfig !== 'object') { + throw new Error( + `Error loading ${chalk.bold(fileConfigPath)}: ` + + `should export an object or a function that returns object.` + ) + } + } + + // package.vue + if (pkgConfig && typeof pkgConfig !== 'object') { + throw new Error( + `Error loading Vue CLI config in ${chalk.bold(`package.json`)}: ` + + `the "vue" field should be an object.` + ) + } + + let resolved, resolvedFrom + if (fileConfig) { + const configFileName = path.basename(fileConfigPath) + if (pkgConfig) { + warn( + `"vue" field in package.json ignored ` + + `due to presence of ${chalk.bold(configFileName)}.` + ) + warn( + `You should migrate it into ${chalk.bold(configFileName)} ` + + `and remove it from package.json.` + ) + } + resolved = fileConfig + resolvedFrom = configFileName + } else if (pkgConfig) { + resolved = pkgConfig + resolvedFrom = '"vue" field in package.json' + } else { + resolved = inlineOptions || {} + resolvedFrom = 'inline options' + } + + // normalize some options + ensureSlash(resolved, 'publicPath') + if (typeof resolved.publicPath === 'string') { + resolved.publicPath = resolved.publicPath.replace(/^\.\//, '') + } + removeSlash(resolved, 'outputDir') + + // validate options + validate(resolved, msg => { + error(`Invalid options in ${chalk.bold(resolvedFrom)}: ${msg}`) + }) + + return resolved +} diff --git a/packages/@vue/cli-service/package.json b/packages/@vue/cli-service/package.json index 42ac1247fa..241d1a229a 100644 --- a/packages/@vue/cli-service/package.json +++ b/packages/@vue/cli-service/package.json @@ -56,6 +56,7 @@ "globby": "^11.0.2", "hash-sum": "^2.0.0", "html-webpack-plugin": "^5.1.0", + "is-file-esm": "^1.0.0", "launch-editor-middleware": "^2.2.1", "lodash.defaultsdeep": "^4.6.1", "lodash.mapvalues": "^4.6.0", diff --git a/yarn.lock b/yarn.lock index 36fb726ac1..225b18ba9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12658,6 +12658,13 @@ is-extglob@^2.1.0, is-extglob@^2.1.1: resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= +is-file-esm@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-file-esm/-/is-file-esm-1.0.0.tgz#987086b0f5a5318179e9d30f4f2f8d37321e1b5f" + integrity sha512-rZlaNKb4Mr8WlRu2A9XdeoKgnO5aA53XdPHgCKVyCrQ/rWi89RET1+bq37Ru46obaQXeiX4vmFIm1vks41hoSA== + dependencies: + read-pkg-up "^7.0.1" + is-finite@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" From 6b801e53f942fa2778346077c5aaab330c212f38 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 7 Apr 2021 17:02:32 +0800 Subject: [PATCH 2/6] test: pass --experimental-vm-modules to node to enable esm tests in jest --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b5826553e1..baf4afdb67 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "packages/vue-cli-version-marker" ], "scripts": { - "test": "node scripts/test.js", + "test": "node --experimental-vm-modules scripts/test.js", "pretest": "yarn clean", "lint": "eslint --fix packages/**/*.js packages/**/bin/*", "lint-without-fix": "eslint packages/**/*.js packages/**/bin/*", From 98020cf4e0ef30caf2846ba408d65f0a05d0b64c Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 7 Apr 2021 17:23:56 +0800 Subject: [PATCH 3/6] test: use `await service.init()` --- .../__tests__/tsPluginBabel.spec.js | 4 +- .../@vue/cli-service/__tests__/css.spec.js | 42 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/@vue/cli-plugin-typescript/__tests__/tsPluginBabel.spec.js b/packages/@vue/cli-plugin-typescript/__tests__/tsPluginBabel.spec.js index e88d243989..4f8583b0a6 100644 --- a/packages/@vue/cli-plugin-typescript/__tests__/tsPluginBabel.spec.js +++ b/packages/@vue/cli-plugin-typescript/__tests__/tsPluginBabel.spec.js @@ -4,7 +4,7 @@ const Service = require('@vue/cli-service/lib/Service') const create = require('@vue/cli-test-utils/createTestProject') const { assertServe, assertBuild } = require('./tsPlugin.helper') -test('using correct loader', () => { +test('using correct loader', async () => { const service = new Service('/', { pkg: {}, plugins: [ @@ -13,7 +13,7 @@ test('using correct loader', () => { ] }) - service.init() + await service.init() const config = service.resolveWebpackConfig() // eslint-disable-next-line no-shadow const rule = config.module.rules.find(rule => rule.test.test('foo.ts')) diff --git a/packages/@vue/cli-service/__tests__/css.spec.js b/packages/@vue/cli-service/__tests__/css.spec.js index c30a497698..68e7d7b73b 100644 --- a/packages/@vue/cli-service/__tests__/css.spec.js +++ b/packages/@vue/cli-service/__tests__/css.spec.js @@ -19,11 +19,11 @@ const LOADERS = { stylus: 'stylus' } -const genConfig = (pkg = {}, env) => { +const genConfig = async (pkg = {}, env) => { const prevEnv = process.env.NODE_ENV if (env) process.env.NODE_ENV = env const service = new Service('/', { pkg }) - service.init() + await service.init() const config = service.resolveWebpackConfig() process.env.NODE_ENV = prevEnv return config @@ -58,8 +58,8 @@ const findOptions = (config, lang, _loader, index) => { return use.options || {} } -test('default loaders', () => { - const config = genConfig() +test('default loaders', async () => { + const config = await genConfig() LANGS.forEach(lang => { const loader = lang === 'css' ? [] : LOADERS[lang] @@ -80,8 +80,8 @@ test('default loaders', () => { }) }) -test('production defaults', () => { - const config = genConfig({}, 'production') +test('production defaults', async () => { + const config = await genConfig({}, 'production') LANGS.forEach(lang => { const loader = lang === 'css' ? [] : LOADERS[lang] expect(findLoaders(config, lang)).toEqual([extractLoaderPath, 'css', 'postcss'].concat(loader)) @@ -93,8 +93,8 @@ test('production defaults', () => { }) }) -test('override postcss config', () => { - const config = genConfig({ postcss: {} }) +test('override postcss config', async () => { + const config = await genConfig({ postcss: {} }) LANGS.forEach(lang => { const loader = lang === 'css' ? [] : LOADERS[lang] expect(findLoaders(config, lang)).toEqual(['vue-style', 'css', 'postcss'].concat(loader)) @@ -107,7 +107,7 @@ test('override postcss config', () => { }) }) -test('Customized CSS Modules rules', () => { +test('Customized CSS Modules rules', async () => { const userOptions = { vue: { css: { @@ -122,7 +122,7 @@ test('Customized CSS Modules rules', () => { } } - const config = genConfig(userOptions) + const config = await genConfig(userOptions) LANGS.forEach(lang => { const expected = { @@ -142,8 +142,8 @@ test('Customized CSS Modules rules', () => { }) }) -test('css.extract', () => { - const config = genConfig({ +test('css.extract', async () => { + const config = await genConfig({ vue: { css: { extract: false @@ -159,7 +159,7 @@ test('css.extract', () => { expect(findOptions(config, lang, 'postcss').postcssOptions.plugins).toBeTruthy() }) - const config2 = genConfig({ + const config2 = await genConfig({ postcss: {}, vue: { css: { @@ -178,8 +178,8 @@ test('css.extract', () => { }) }) -test('css.sourceMap', () => { - const config = genConfig({ +test('css.sourceMap', async () => { + const config = await genConfig({ postcss: {}, vue: { css: { @@ -194,9 +194,9 @@ test('css.sourceMap', () => { }) }) -test('css-loader options', () => { +test('css-loader options', async () => { const localIdentName = '[name]__[local]--[hash:base64:5]' - const config = genConfig({ + const config = await genConfig({ vue: { css: { loaderOptions: { @@ -219,9 +219,9 @@ test('css-loader options', () => { }) }) -test('css.loaderOptions', () => { +test('css.loaderOptions', async () => { const prependData = '$env: production;' - const config = genConfig({ + const config = await genConfig({ vue: { css: { loaderOptions: { @@ -254,11 +254,11 @@ test('css.loaderOptions', () => { }) }) -test('scss loaderOptions', () => { +test('scss loaderOptions', async () => { const sassData = '$env: production' const scssData = '$env: production;' - const config = genConfig({ + const config = await genConfig({ vue: { css: { loaderOptions: { From 0b9bdd8e2fda1850478a76fb2d7f80f697508056 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 7 Apr 2021 17:53:19 +0800 Subject: [PATCH 4/6] test: use await for service.init and service.run --- .../cli-service/__tests__/Service.spec.js | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/packages/@vue/cli-service/__tests__/Service.spec.js b/packages/@vue/cli-service/__tests__/Service.spec.js index a30228622e..5d4c6cf9d0 100644 --- a/packages/@vue/cli-service/__tests__/Service.spec.js +++ b/packages/@vue/cli-service/__tests__/Service.spec.js @@ -10,13 +10,13 @@ const mockPkg = json => { fs.writeFileSync('/package.json', JSON.stringify(json, null, 2)) } -const createMockService = (plugins = [], init = true, mode) => { +const createMockService = async (plugins = [], init = true, mode) => { const service = new Service('/', { plugins, useBuiltIn: false }) if (init) { - service.init(mode) + await service.init(mode) } return service } @@ -36,11 +36,11 @@ afterEach(() => { } }) -test('env loading', () => { +test('env loading', async () => { process.env.FOO = 0 fs.writeFileSync('/.env.local', `FOO=1\nBAR=2`) fs.writeFileSync('/.env', `BAR=3\nBAZ=4`) - createMockService() + await createMockService() expect(process.env.FOO).toBe('0') expect(process.env.BAR).toBe('2') @@ -50,11 +50,11 @@ test('env loading', () => { fs.unlinkSync('/.env') }) -test('env loading for custom mode', () => { +test('env loading for custom mode', async () => { process.env.VUE_CLI_TEST_TESTING_ENV = true fs.writeFileSync('/.env', 'FOO=1') fs.writeFileSync('/.env.staging', 'FOO=2\nNODE_ENV=production') - createMockService([], true, 'staging') + await createMockService([], true, 'staging') expect(process.env.FOO).toBe('2') expect(process.env.NODE_ENV).toBe('production') @@ -78,59 +78,59 @@ test('loading plugins from package.json', () => { expect(service.plugins.some(({ id }) => id === 'bar')).toBe(false) }) -test('load project options from package.json', () => { +test('load project options from package.json', async () => { mockPkg({ vue: { lintOnSave: 'default' } }) - const service = createMockService() + const service = await createMockService() expect(service.projectOptions.lintOnSave).toBe('default') }) -test('handle option publicPath and outputDir correctly', () => { +test('handle option publicPath and outputDir correctly', async () => { mockPkg({ vue: { publicPath: 'https://foo.com/bar', outputDir: '/public/' } }) - const service = createMockService() + const service = await createMockService() expect(service.projectOptions.publicPath).toBe('https://foo.com/bar/') expect(service.projectOptions.outputDir).toBe('/public') }) -test('normalize publicPath when relative', () => { +test('normalize publicPath when relative', async () => { mockPkg({ vue: { publicPath: './foo/bar' } }) - const service = createMockService() + const service = await createMockService() expect(service.projectOptions.publicPath).toBe('foo/bar/') }) -test('allow custom protocol in publicPath', () => { +test('allow custom protocol in publicPath', async () => { mockPkg({ vue: { publicPath: 'customprotocol://foo/bar' } }) - const service = createMockService() + const service = await createMockService() expect(service.projectOptions.publicPath).toBe('customprotocol://foo/bar/') }) -test('keep publicPath when empty', () => { +test('keep publicPath when empty', async () => { mockPkg({ vue: { publicPath: '' } }) - const service = createMockService() + const service = await createMockService() expect(service.projectOptions.publicPath).toBe('') }) -test('load project options from vue.config.js', () => { +test('load project options from vue.config.js', async () => { fs.writeFileSync(path.resolve('/', 'vue.config.js'), '') // only to ensure fs.existsSync returns true jest.mock(path.resolve('/', 'vue.config.js'), () => ({ lintOnSave: false }), { virtual: true }) mockPkg({ @@ -138,12 +138,12 @@ test('load project options from vue.config.js', () => { lintOnSave: 'default' } }) - const service = createMockService() + const service = await createMockService() // vue.config.js has higher priority expect(service.projectOptions.lintOnSave).toBe(false) }) -test('load project options from vue.config.js as a function', () => { +test('load project options from vue.config.js as a function', async () => { fs.writeFileSync(path.resolve('/', 'vue.config.js'), '') jest.mock(path.resolve('/', 'vue.config.js'), () => function () { return { lintOnSave: false } }, { virtual: true }) mockPkg({ @@ -151,12 +151,12 @@ test('load project options from vue.config.js as a function', () => { lintOnSave: 'default' } }) - const service = createMockService() + const service = await createMockService() // vue.config.js has higher priority expect(service.projectOptions.lintOnSave).toBe(false) }) -test('api: assertVersion', () => { +test('api: assertVersion', async () => { const plugin = { id: 'test-assertVersion', apply: api => { @@ -169,12 +169,12 @@ test('api: assertVersion', () => { expect(() => api.assertVersion('^100')).toThrow('Require @vue/cli-service "^100"') } } - createMockService([plugin], true /* init */) + await createMockService([plugin], true /* init */) }) -test('api: registerCommand', () => { +test('api: registerCommand', async () => { let args - const service = createMockService([{ + const service = await createMockService([{ id: 'test', apply: api => { api.registerCommand('foo', _args => { @@ -183,13 +183,13 @@ test('api: registerCommand', () => { } }]) - service.run('foo', { n: 1 }) + await service.run('foo', { n: 1 }) expect(args).toEqual({ _: [], n: 1 }) }) -test('api: --skip-plugins', () => { +test('api: --skip-plugins', async () => { let untouched = true - const service = createMockService([{ + const service = await createMockService([{ id: 'test-command', apply: api => { api.registerCommand('foo', _args => { @@ -204,11 +204,11 @@ test('api: --skip-plugins', () => { } }], false) - service.run('foo', { 'skip-plugins': 'test-plugin' }) + await service.run('foo', { 'skip-plugins': 'test-plugin' }) expect(untouched).toEqual(true) }) -test('api: defaultModes', () => { +test('api: defaultModes', async () => { fs.writeFileSync('/.env.foo', `FOO=5\nBAR=6`) fs.writeFileSync('/.env.foo.local', `FOO=7\nBAZ=8`) @@ -229,7 +229,7 @@ test('api: defaultModes', () => { foo: 'foo' } - createMockService([plugin1], false /* init */).run('foo') + await (await createMockService([plugin1], false /* init */)).run('foo') delete process.env.NODE_ENV delete process.env.BABEL_ENV @@ -246,11 +246,11 @@ test('api: defaultModes', () => { test: 'test' } - createMockService([plugin2], false /* init */).run('test') + await (await createMockService([plugin2], false /* init */)).run('test') }) -test('api: chainWebpack', () => { - const service = createMockService([{ +test('api: chainWebpack', async () => { + const service = await createMockService([{ id: 'test', apply: api => { api.chainWebpack(config => { @@ -263,8 +263,8 @@ test('api: chainWebpack', () => { expect(config.output.path).toBe('test-dist') }) -test('api: configureWebpack', () => { - const service = createMockService([{ +test('api: configureWebpack', async () => { + const service = await createMockService([{ id: 'test', apply: api => { api.configureWebpack(config => { @@ -279,8 +279,8 @@ test('api: configureWebpack', () => { expect(config.output.path).toBe('test-dist-2') }) -test('api: configureWebpack returning object', () => { - const service = createMockService([{ +test('api: configureWebpack returning object', async () => { + const service = await createMockService([{ id: 'test', apply: api => { api.configureWebpack(config => { @@ -297,8 +297,8 @@ test('api: configureWebpack returning object', () => { expect(config.output.path).toBe('test-dist-3') }) -test('api: configureWebpack preserve ruleNames', () => { - const service = createMockService([ +test('api: configureWebpack preserve ruleNames', async () => { + const service = await createMockService([ { id: 'babel', apply: require('@vue/cli-plugin-babel') @@ -319,10 +319,10 @@ test('api: configureWebpack preserve ruleNames', () => { expect(config.module.rules[0].__ruleNames).toEqual(['js']) }) -test('internal: should correctly set VUE_CLI_ENTRY_FILES', () => { +test('internal: should correctly set VUE_CLI_ENTRY_FILES', async () => { delete process.env.VUE_CLI_ENTRY_FILES - const service = createMockService([{ + const service = await createMockService([{ id: 'test', apply: api => { api.configureWebpack(config => { @@ -343,9 +343,9 @@ test('internal: should correctly set VUE_CLI_ENTRY_FILES', () => { ) }) -test('api: configureDevServer', () => { +test('api: configureDevServer', async () => { const cb = () => {} - const service = createMockService([{ + const service = await createMockService([{ id: 'test', apply: api => { api.configureDevServer(cb) @@ -354,8 +354,8 @@ test('api: configureDevServer', () => { expect(service.devServerConfigFns).toContain(cb) }) -test('api: resolve', () => { - createMockService([{ +test('api: resolve', async () => { + await createMockService([{ id: 'test', apply: api => { expect(api.resolve('foo.js')).toBe(path.resolve('/', 'foo.js')) @@ -363,8 +363,8 @@ test('api: resolve', () => { }]) }) -test('api: hasPlugin', () => { - createMockService([ +test('api: hasPlugin', async () => { + await createMockService([ { id: 'vue-cli-plugin-foo', apply: api => { From 0e794c6cd36d59b6788d0789f52dcc1d551ec000 Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Wed, 7 Apr 2021 20:53:36 +0800 Subject: [PATCH 5/6] test: try a longer timeout --- packages/@vue/cli-service/__tests__/ServiceESM.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/@vue/cli-service/__tests__/ServiceESM.spec.js b/packages/@vue/cli-service/__tests__/ServiceESM.spec.js index 5bb0546d43..92014eafdb 100644 --- a/packages/@vue/cli-service/__tests__/ServiceESM.spec.js +++ b/packages/@vue/cli-service/__tests__/ServiceESM.spec.js @@ -1,3 +1,4 @@ +jest.setTimeout(200000) const path = require('path') const fs = require('fs-extra') From f3ec4d8b3f23ca57a6799a1e90db55787a76924c Mon Sep 17 00:00:00 2001 From: Haoqun Jiang Date: Thu, 8 Apr 2021 12:00:41 +0800 Subject: [PATCH 6/6] fix: use callback in `init()` to fix cases where `.init()` must be synchronous Such as in the `webpack.config.js` --- packages/@vue/cli-service/lib/Service.js | 46 +++++++++++++----------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/@vue/cli-service/lib/Service.js b/packages/@vue/cli-service/lib/Service.js index da8e826859..5974a8cd1a 100644 --- a/packages/@vue/cli-service/lib/Service.js +++ b/packages/@vue/cli-service/lib/Service.js @@ -13,6 +13,8 @@ const checkWebpack = require('./util/checkWebpack') const loadFileConfig = require('./util/loadFileConfig') const resolveUserConfig = require('./util/resolveUserConfig') +// Seems we can't use `instanceof Promise` here (would fail the tests) +const isPromise = p => p && typeof p.then === 'function' module.exports = class Service { constructor (context, { plugins, pkg, inlineOptions, useBuiltIn } = {}) { checkWebpack(context) @@ -56,7 +58,7 @@ module.exports = class Service { return pkg } - async init (mode = process.env.VUE_CLI_MODE) { + init (mode = process.env.VUE_CLI_MODE) { if (this.initialized) { return } @@ -71,23 +73,31 @@ module.exports = class Service { this.loadEnv() // load user config - const userOptions = await this.loadUserOptions() - this.projectOptions = defaultsDeep(userOptions, defaults()) + const userOptions = this.loadUserOptions() + const loadedCallback = (loadedUserOptions) => { + this.projectOptions = defaultsDeep(loadedUserOptions, defaults()) - debug('vue:project-config')(this.projectOptions) + debug('vue:project-config')(this.projectOptions) - // apply plugins. - this.plugins.forEach(({ id, apply }) => { - if (this.pluginsToSkip.has(id)) return - apply(new PluginAPI(id, this), this.projectOptions) - }) + // apply plugins. + this.plugins.forEach(({ id, apply }) => { + if (this.pluginsToSkip.has(id)) return + apply(new PluginAPI(id, this), this.projectOptions) + }) - // apply webpack configs from project config file - if (this.projectOptions.chainWebpack) { - this.webpackChainFns.push(this.projectOptions.chainWebpack) + // apply webpack configs from project config file + if (this.projectOptions.chainWebpack) { + this.webpackChainFns.push(this.projectOptions.chainWebpack) + } + if (this.projectOptions.configureWebpack) { + this.webpackRawConfigFns.push(this.projectOptions.configureWebpack) + } } - if (this.projectOptions.configureWebpack) { - this.webpackRawConfigFns.push(this.projectOptions.configureWebpack) + + if (isPromise(userOptions)) { + return userOptions.then(loadedCallback) + } else { + return loadedCallback(userOptions) } } @@ -322,13 +332,9 @@ module.exports = class Service { loadUserOptions () { const { fileConfig, fileConfigPath } = loadFileConfig(this.context) - // Seems we can't use `instanceof Promise` here (would fail the tests) - if (fileConfig && typeof fileConfig.then === 'function') { + if (isPromise(fileConfig)) { return fileConfig - .then(mod => { - // fs.writeFileSync(`${this.context}/aaaa`, `mod ${JSON.stringify(mod, null, 2)}`) - return mod.default - }) + .then(mod => mod.default) .then(loadedConfig => resolveUserConfig({ inlineOptions: this.inlineOptions, pkgConfig: this.pkg.vue,