feat: .nuxtignore (#4647)

This commit is contained in:
Xin Du (Clark) 2019-01-29 09:31:14 +00:00 committed by GitHub
parent a303ea89b3
commit 59be77a2f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 178 additions and 101 deletions

View File

@ -16,6 +16,7 @@
"fs-extra": "^7.0.1",
"glob": "^7.1.3",
"hash-sum": "^1.0.2",
"ignore": "^5.0.4",
"lodash": "^4.17.11",
"pify": "^4.0.1",
"semver": "^5.6.0",

View File

@ -31,6 +31,7 @@ import {
isIndexFileAndFolder
} from '@nuxt/utils'
import Ignore from './ignore'
import BuildContext from './context'
const glob = pify(Glob)
@ -86,6 +87,9 @@ export default class Builder {
// }
this.bundleBuilder = this.getBundleBuilder(bundleBuilder)
this.ignore = new Ignore({
rootDir: this.options.srcDir
})
}
getBundleBuilder(BundleBuilder) {
@ -132,6 +136,18 @@ export default class Builder {
)
}
async resolveFiles(dir, cwd = this.options.srcDir) {
return this.ignore.filter(await glob(`${dir}/**/*.{${this.supportedExtensions.join(',')}}`, {
cwd,
ignore: this.options.ignore
}))
}
async resolveRelative(dir) {
const dirPrefix = new RegExp(`^${dir}/`)
return (await this.resolveFiles(dir)).map(file => ({ src: file.replace(dirPrefix, '') }))
}
resolvePlugins() {
// Check plugins exist then set alias to their real path
return Promise.all(this.plugins.map(async (p) => {
@ -318,14 +334,12 @@ export default class Builder {
router: this.options.router,
env: this.options.env,
head: this.options.head,
middleware: fsExtra.existsSync(path.join(this.options.srcDir, this.options.dir.middleware)),
store: this.options.store,
globalName: this.options.globalName,
globals: this.globals,
css: this.options.css,
plugins: this.plugins,
appPath: './App.js',
ignorePrefix: this.options.ignorePrefix,
layouts: Object.assign({}, this.options.layouts),
loading:
typeof this.options.loading === 'string'
@ -344,10 +358,7 @@ export default class Builder {
// -- Layouts --
if (fsExtra.existsSync(path.resolve(this.options.srcDir, this.options.dir.layouts))) {
const configLayouts = this.options.layouts
const layoutsFiles = await glob(`${this.options.dir.layouts}/**/*.{${this.supportedExtensions.join(',')}}`, {
cwd: this.options.srcDir,
ignore: this.options.ignore
})
const layoutsFiles = await this.resolveFiles(this.options.dir.layouts)
layoutsFiles.forEach((file) => {
const name = file
.replace(new RegExp(`^${this.options.dir.layouts}/`), '')
@ -392,16 +403,14 @@ export default class Builder {
} else if (this._nuxtPages) {
// Use nuxt.js createRoutes bases on pages/
const files = {}
;(await glob(`${this.options.dir.pages}/**/*.{${this.supportedExtensions.join(',')}}`, {
cwd: this.options.srcDir,
ignore: this.options.ignore
})).forEach((f) => {
const key = f.replace(new RegExp(`\\.(${this.supportedExtensions.join('|')})$`), '')
const ext = new RegExp(`\\.(${this.supportedExtensions.join('|')})$`)
for (const page of await this.resolveFiles(this.options.dir.pages)) {
const key = page.replace(ext, '')
// .vue file takes precedence over other extensions
if (/\.vue$/.test(f) || !files[key]) {
files[key] = f.replace(/(['"])/g, '\\$1')
if (/\.vue$/.test(page) || !files[key]) {
files[key] = page.replace(/(['"])/g, '\\$1')
}
})
}
templateVars.router.routes = createRoutes(
Object.values(files),
this.options.srcDir,
@ -438,9 +447,24 @@ export default class Builder {
// -- Store --
// Add store if needed
if (this.options.store) {
templateVars.storeModules = (await this.resolveRelative(this.options.dir.store))
.sort(({ src: p1 }, { src: p2 }) => {
// modules are sorted from low to high priority (for overwriting properties)
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
})
templatesFiles.push('store.js')
}
// -- Middleware --
templateVars.middleware = await this.resolveRelative(this.options.dir.middleware)
// Resolve template files
const customTemplateFiles = this.options.build.templates.map(
t => t.dst || path.basename(t.src || t)
@ -676,6 +700,10 @@ export default class Builder {
...this.options.watch
].map(this.nuxt.resolver.resolveAlias)
if (this.ignore.ignoreFile) {
nuxtRestartWatch.push(this.ignore.ignoreFile)
}
this.watchers.restart = chokidar
.watch(nuxtRestartWatch, this.options.watchers.chokidar)
.on('all', (event, _path) => {

View File

@ -0,0 +1,51 @@
import path from 'path'
import fs from 'fs-extra'
import ignore from 'ignore'
export default class Ignore {
constructor(options) {
this.rootDir = options.rootDir
this.addIgnoresRules()
}
static get IGNORE_FILENAME() {
return '.nuxtignore'
}
findIgnoreFile() {
if (!this.ignoreFile) {
const ignoreFile = path.resolve(this.rootDir, Ignore.IGNORE_FILENAME)
if (fs.existsSync(ignoreFile) && fs.statSync(ignoreFile).isFile()) {
this.ignoreFile = ignoreFile
this.ignore = ignore()
}
}
return this.ignoreFile
}
readIgnoreFile() {
if (this.findIgnoreFile()) {
return fs.readFileSync(this.ignoreFile, 'utf8')
}
}
addIgnoresRules() {
const content = this.readIgnoreFile()
if (content) {
this.ignore.add(content)
}
}
filter(paths) {
if (this.ignore) {
return this.ignore.filter([].concat(paths || []))
}
return paths
}
reload() {
delete this.ignore
delete this.ignoreFile
this.addIgnoresRules()
}
}

View File

@ -1,18 +1,8 @@
<% if (middleware) { %>
const files = require.context('@/<%= dir.middleware %>', false, /^\.\/(?!<%= ignorePrefix %>)[^.]+\.(<%= extensions %>)$/)
const filenames = files.keys()
function getModule(filename) {
const file = files(filename)
return file.default || file
}
const middleware = {}
// Generate the middleware
for (const filename of filenames) {
const name = filename.replace(/^\.\//, '').replace(/\.(<%= extensions %>)$/, '')
middleware[name] = getModule(filename)
}
<% middleware.forEach(m => {
const name = m.src.replace(new RegExp(`\\.(${extensions})$`), '')
%>
middleware['<%= name %>'] = require('@/<%= dir.middleware %>/<%= m.src %>');
middleware['<%= name %>'] = middleware['<%= name %>'].default || middleware['<%= name %>']
<% }) %>
export default middleware
<% } else { %>export default {}<% } %>

View File

@ -6,29 +6,12 @@ Vue.use(Vuex)
const log = console // on server-side, consola will catch all console.log
const VUEX_PROPERTIES = ['state', 'getters', 'actions', 'mutations']
let store = {}
let fileResolver
void (function updateModules() {
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 indexPath = paths.find(path => path.includes('./index.'))
if (indexPath) {
store = requireModule(indexPath, { isRoot: true })
}
<% storeModules.some(s => {
if(s.src.indexOf('index.') === 0) { %>
store = normalizeRoot(require('@/<%= dir.store %>/<%= s.src %>'), '<%= dir.store %>/<%= s.src %>')
<% return true }}) %>
// If store is an exported method = classic mode (deprecated)
if (typeof store === 'function') {
@ -38,47 +21,17 @@ void (function updateModules() {
// 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 %>)$/, '')
<% storeModules.forEach(s => {
if(s.src.indexOf('index.') !== 0) { %>
resolveStoreModules(require('@/<%= dir.store %>/<%= s.src %>'), '<%= s.src %>')<% }}) %>
// 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, () => {
module.hot.accept([<% storeModules.forEach(s => { %>
'@/<%= dir.store %>/<%= s.src %>',<% }) %>
], () => {
// Update `root.modules` with the latest definitions.
updateModules()
// Trigger a hot update in the store.
@ -94,26 +47,68 @@ export const createStore = store instanceof Function ? store : () => {
}, store))
}
// Dynamically require module
function requireModule(path, { isRoot = false, isState = false } = {}) {
const file = fileResolver(path)
let moduleData = file.default || file
function resolveStoreModules(moduleData, filename) {
moduleData = moduleData.default || moduleData
// Remove store src + extension (./foo/index.js -> foo/index)
const namespace = filename.replace(/\.(<%= extensions %>)$/, '')
const namespaces = namespace.split('/')
let moduleName = namespaces[namespaces.length - 1]
const filePath = `<%= dir.store %>/${filename}`
if (isState && typeof moduleData !== 'function') {
log.warn(`${path} should export a method that returns an object`)
const state = Object.assign({}, moduleData)
return () => state
}
if (isRoot && moduleData.commit) {
throw new Error('[nuxt] <%= dir.store %>/' + path.replace('./', '') + ' should export a method that returns a Vuex instance.')
moduleData = moduleName === 'state'
? normalizeState(moduleData, filePath)
: normalizeModule(moduleData, filePath)
// If src 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)
return
}
if (isRoot && typeof moduleData !== 'function') {
// 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)
}
}
function normalizeRoot(moduleData, filePath) {
moduleData = moduleData.default || moduleData
if (moduleData.commit) {
throw new Error(`[nuxt] ${filePath} should export a method that returns a Vuex instance.`)
}
if (typeof moduleData !== 'function') {
// Avoid TypeError: setting a property that has only a getter when overwriting top level keys
moduleData = Object.assign({}, moduleData)
}
return normalizeModule(moduleData, filePath)
}
function normalizeState(moduleData, filePath) {
if (typeof moduleData !== 'function') {
log.warn(`${filePath} should export a method that returns an object`)
const state = Object.assign({}, moduleData)
return () => state
}
return normalizeModule(moduleData, filePath)
}
function normalizeModule(moduleData, filePath) {
if (moduleData.state && typeof moduleData.state !== 'function') {
log.warn(`'state' should be a method that returns an object in ${path}`)
log.warn(`'state' should be a method that returns an object in ${filePath}`)
const state = Object.assign({}, moduleData.state)
// Avoid TypeError: setting a property that has only a getter when overwriting top level keys
moduleData = Object.assign({}, moduleData, { state: () => state })

4
test/fixtures/with-config/.nuxtignore vendored Normal file
View File

@ -0,0 +1,4 @@
layouts/test-ignore.vue
pages/test-ignore.vue
store/test-ignore.js
middleware/test-ignore.js

View File

@ -0,0 +1 @@
throw new Error('This file should be ignored!!')

View File

@ -0,0 +1 @@
throw new Error('This file should be ignored!!')

View File

View File

@ -0,0 +1 @@
throw new Error('This file should be ignored!!')

View File

@ -180,6 +180,11 @@ describe('with-config', () => {
.rejects.toMatchObject({ statusCode: 404 })
})
test('should ignore files in .nuxtignore', async () => {
await expect(rp(url('/test-ignore')))
.rejects.toMatchObject({ statusCode: 404 })
})
test('renderAndGetWindow options', async () => {
const fakeErrorLog = jest.fn()
const mockOptions = {

View File

@ -5478,7 +5478,7 @@ ignore@^4.0.6:
resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.0.2:
ignore@^5.0.2, ignore@^5.0.4:
version "5.0.4"
resolved "https://registry.npmjs.org/ignore/-/ignore-5.0.4.tgz#33168af4a21e99b00c5d41cbadb6a6cb49903a45"
integrity sha512-WLsTMEhsQuXpCiG173+f3aymI43SXa+fB1rSfbzyP4GkPP+ZFVuO0/3sFUGNBtifisPeDcl/uD/Y2NxZ7xFq4g==