From deadc48f19140c4430c5db0bf7b6af6e033d1501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Chopin?= Date: Fri, 18 Jan 2019 12:28:34 +0100 Subject: [PATCH] fix(vue-app): Fix Vuex HMR & refactor for better modules usage (#4791) * fix(vue-app): Refactor store for better modules usage * Update packages/vue-app/template/store.js Co-Authored-By: Atinux * fix: tests and code style --- packages/vue-app/template/store.js | 212 +++++++++++++++-------------- 1 file changed, 110 insertions(+), 102 deletions(-) diff --git a/packages/vue-app/template/store.js b/packages/vue-app/template/store.js index 37345e981f..869e663b26 100644 --- a/packages/vue-app/template/store.js +++ b/packages/vue-app/template/store.js @@ -3,130 +3,138 @@ import Vuex from 'vuex' Vue.use(Vuex) -let storeData = {} - -let files +const VUEX_PROPERTIES = ['state', 'getters', 'actions', 'mutations'] +let store = {} +let fileResolver void (function updateModules() { - files = require.context('@/<%= dir.store %>', true, /^\.\/(?!<%= ignorePrefix %>)[^.]+\.(<%= extensions %>)$/) - const filenames = files.keys() + fileResolver = require.context('@/<%= dir.store %>', true, /^\.\/(?!<%= ignorePrefix %>)[^.]+\.(<%= extensions %>)$/) + + // Paths are sorted from low to high priority (for overwriting properties) + const paths = fileResolver.keys().sort((p1, p2) => { + let res = p1.split('/').length - p2.split('/').length + + if (res === 0 && p1.includes('/index.')) { + res = -1 + } else if (res === 0 && p2.includes('/index.')) { + res = 1 + } + return res + }) // Check if {dir.store}/index.js exists - const indexFilename = filenames.find(filename => filename.includes('./index.')) + const indexPath = paths.find(path => path.includes('./index.')) - if (indexFilename) { - storeData = getModule(indexFilename) + if (indexPath) { + store = requireModule(indexPath, { isRoot: true }) } - // If store is not an exported method = modules store - if (typeof storeData !== 'function') { - // Store modules - if (!storeData.modules) { - storeData.modules = {} - } - - for (const filename of filenames) { - let name = filename.replace(/^\.\//, '').replace(/\.(<%= extensions %>)$/, '') - if (name === 'index') continue - - const namePath = name.split(/\//) - - name = namePath[namePath.length - 1] - if (['state', 'getters', 'actions', 'mutations'].includes(name)) { - const module = getModuleNamespace(storeData, namePath, true) - appendModule(module, filename, name) - continue - } - - // If file is foo/index.js, it should be saved as foo - const isIndex = (name === 'index') - if (isIndex) { - namePath.pop() - } - - const module = getModuleNamespace(storeData, namePath) - const fileModule = getModule(filename) - - name = namePath.pop() - module[name] = module[name] || {} - - // if file is foo.js, existing properties take priority - // because it's the least specific case - if (!isIndex) { - module[name] = Object.assign({}, fileModule, module[name]) - module[name].namespaced = true - continue - } - - // if file is foo/index.js we want to overwrite properties from foo.js - // but not from appended mods like foo/actions.js - const appendedMods = {} - if (module[name].appends) { - appendedMods.appends = module[name].appends - for (const append of module[name].appends) { - appendedMods[append] = module[name][append] - } - } - - module[name] = Object.assign({}, module[name], fileModule, appendedMods) - module[name].namespaced = true - } - // If the environment supports hot reloading... - <% if (isDev) { %> - if (process.client && module.hot) { - // Whenever any Vuex module is updated... - module.hot.accept(files.id, () => { - // Update `root.modules` with the latest definitions. - updateModules() - // Trigger a hot update in the store. - window.<%= globals.nuxt %>.$store.hotUpdate(storeData) - }) - }<% } %> - } else { + // If store is an exported method = classic mode (deprecated) + if (typeof store === 'function') { const log = (process.server ? require('consola') : console) - log.warn('Classic mode for store/ is deprecated and will be removed in Nuxt 3.') + return log.warn('Classic mode for store/ is deprecated and will be removed in Nuxt 3.') } + + // Enforce store modules + store.modules = store.modules || {} + + for (const path of paths) { + // Remove store path + extension (./foo/index.js -> foo/index) + const namespace = path.replace(/^\.\//, '').replace(/\.(<%= extensions %>)$/, '') + + // Ignore indexFile, handled before + if (namespace === 'index') { + continue + } + + const namespaces = namespace.split('/') + let moduleName = namespaces[namespaces.length - 1] + const moduleData = requireModule(path, { isState: moduleName === 'state' }) + + // If path is a known Vuex property + if (VUEX_PROPERTIES.includes(moduleName)) { + const property = moduleName + const storeModule = getStoreModule(store, namespaces, { isProperty: true }) + + // Replace state since it's a function + mergeProperty(storeModule, moduleData, property) + continue + } + + // If file is foo/index.js, it should be saved as foo + const isIndexModule = (moduleName === 'index') + if (isIndexModule) { + namespaces.pop() + moduleName = namespaces[namespaces.length - 1] + } + + const storeModule = getStoreModule(store, namespaces) + + for (const property of VUEX_PROPERTIES) { + mergeProperty(storeModule, moduleData[property], property) + } + } + // If the environment supports hot reloading... + <% if (isDev) { %> + if (process.client && module.hot) { + // Whenever any Vuex module is updated... + module.hot.accept(fileResolver.id, () => { + // Update `root.modules` with the latest definitions. + updateModules() + // Trigger a hot update in the store. + window.<%= globals.nuxt %>.$store.hotUpdate(store) + }) + }<% } %> })() // createStore -export const createStore = storeData instanceof Function ? storeData : () => { +export const createStore = store instanceof Function ? store : () => { return new Vuex.Store(Object.assign({ strict: (process.env.NODE_ENV !== 'production') - }, storeData, { - state: storeData.state instanceof Function ? storeData.state() : {} - })) + }, store)) } // Dynamically require module -function getModule(filename) { - const file = files(filename) - const module = file.default || file - if (module.commit) { - throw new Error('[nuxt] <%= dir.store %>/' + filename.replace('./', '') + ' should export a method which returns a Vuex instance.') +function requireModule(path, { isRoot = false, isState = false } = {}) { + const file = fileResolver(path) + const moduleData = file.default || file + + if (isState && typeof moduleData !== 'function') { + return () => moduleData } - if (module.state && typeof module.state !== 'function') { - throw new Error('[nuxt] state should be a function in <%= dir.store %>/' + filename.replace('./', '')) + if (isRoot && moduleData.commit) { + throw new Error('[nuxt] <%= dir.store %>/' + path.replace('./', '') + ' should export a method which returns a Vuex instance.') } - return module + + if (isRoot && typeof moduleData !== 'function') { + // Avoid TypeError: setting a property that has only a getter when overwriting top level keys + const state = moduleData.state && typeof moduleData.state !== 'function' ? (() => state) : moduleData.state + return Object.assign({}, moduleData, { state }) + } + return moduleData } -function getModuleNamespace(storeData, namePath, forAppend = false) { - if (namePath.length === 1) { - if (forAppend) { - return storeData - } - return storeData.modules +function getStoreModule(storeModule, namespaces, { isProperty = false } = {}) { + // If ./mutations.js + if (!namespaces.length || (isProperty && namespaces.length === 1)) { + return storeModule } - const namespace = namePath.shift() - storeData.modules[namespace] = storeData.modules[namespace] || {} - storeData.modules[namespace].namespaced = true - storeData.modules[namespace].modules = storeData.modules[namespace].modules || {} - return getModuleNamespace(storeData.modules[namespace], namePath, forAppend) + + const namespace = namespaces.shift() + + storeModule.modules[namespace] = storeModule.modules[namespace] || {} + storeModule.modules[namespace].namespaced = true + storeModule.modules[namespace].modules = storeModule.modules[namespace].modules || {} + + return getStoreModule(storeModule.modules[namespace], namespaces, { isProperty }) } -function appendModule(module, filename, name) { - const file = files(filename) - module.appends = module.appends || [] - module.appends.push(name) - module[name] = file.default || file +function mergeProperty(storeModule, moduleData, property) { + if (!moduleData) return + + if (property === 'state') { + storeModule.state = moduleData || storeModule.state + } else { + storeModule[property] = Object.assign({}, storeModule[property], moduleData) + } }