diff --git a/lib/builder/generator.js b/lib/builder/generator.js index e7c022531a..b3ee98e1b7 100644 --- a/lib/builder/generator.js +++ b/lib/builder/generator.js @@ -15,7 +15,7 @@ export default class Generator { // Set variables this.staticRoutes = path.resolve(this.options.srcDir, this.options.dir.static) this.srcBuiltPath = path.resolve(this.options.buildDir, 'dist', 'client') - this.distPath = path.resolve(this.options.rootDir, this.options.generate.dir) + this.distPath = this.options.generate.dir this.distNuxtPath = path.join( this.distPath, isUrl(this.options.build.publicPath) ? '' : this.options.build.publicPath diff --git a/lib/common/options.js b/lib/common/options.js index a7c5a039b1..86749abad5 100644 --- a/lib/common/options.js +++ b/lib/common/options.js @@ -3,11 +3,14 @@ import fs from 'fs' import _ from 'lodash' import consola from 'consola' -import { isPureObject, isUrl } from '../common/utils' +import { isPureObject, isUrl, guardDir } from '../common/utils' import modes from './modes' import defaults from './nuxt.config' +// hasValue utility +const hasValue = v => typeof v === 'string' && v + const Options = {} export default Options @@ -40,8 +43,8 @@ Options.from = function (_options) { options.extensions = [options.extensions] } - const hasValue = v => typeof v === 'string' && v - options.rootDir = hasValue(options.rootDir) ? options.rootDir : process.cwd() + // Resolve rootDir + options.rootDir = hasValue(options.rootDir) ? path.resolve(options.rootDir) : process.cwd() // Apply defaults by ${buildDir}/dist/build.config.js // TODO: Unsafe operation. @@ -54,12 +57,39 @@ Options.from = function (_options) { // Apply defaults _.defaultsDeep(options, defaults) - // Resolve dirs - options.srcDir = hasValue(options.srcDir) + // Check srcDir and generate.dir excistence + const hasSrcDir = hasValue(options.srcDir) + const hasGenerateDir = hasValue(options.generate.dir) + + // Resolve srcDir + options.srcDir = hasSrcDir ? path.resolve(options.rootDir, options.srcDir) : options.rootDir + + // Resolve buildDir options.buildDir = path.resolve(options.rootDir, options.buildDir) + // Protect rootDir against buildDir + guardDir(options, 'rootDir', 'buildDir') + + if (hasGenerateDir) { + // Resolve generate.dir + options.generate.dir = path.resolve(options.rootDir, options.generate.dir) + + // Protect rootDir against buildDir + guardDir(options, 'rootDir', 'generate.dir') + } + + if (hasSrcDir) { + // Protect srcDir against buildDir + guardDir(options, 'srcDir', 'buildDir') + + if (hasGenerateDir) { + // Protect srcDir against generate.dir + guardDir(options, 'srcDir', 'generate.dir') + } + } + // Populate modulesDir options.modulesDir = [] .concat(options.modulesDir) diff --git a/lib/common/utils.js b/lib/common/utils.js index 2b553501b7..023677c5ee 100644 --- a/lib/common/utils.js +++ b/lib/common/utils.js @@ -1,5 +1,6 @@ import path from 'path' import _ from 'lodash' +import consola from 'consola' export const encodeHtml = function encodeHtml(str) { return str.replace(//g, '>') @@ -326,3 +327,25 @@ export const createRoutes = function createRoutes(files, srcDir, pagesDir) { }) return cleanChildrenRoutes(routes) } + +// Guard dir1 from dir2 which can be indiscriminately removed +export const guardDir = function guardDir(options, key1, key2) { + const dir1 = _.get(options, key1, false) + const dir2 = _.get(options, key2, false) + + if ( + dir1 && + dir2 && + ( + dir1 === dir2 || + ( + dir1.startsWith(dir2) && + !path.basename(dir1).startsWith(path.basename(dir2)) + ) + ) + ) { + const errorMessage = `options.${key2} cannot be a parent of or same as ${key1}` + consola.fatal(errorMessage) + throw new Error(errorMessage) + } +} diff --git a/test/unit/basic.fail.generate.test.js b/test/unit/basic.fail.generate.test.js index e109bf5d98..fa3b12b200 100644 --- a/test/unit/basic.fail.generate.test.js +++ b/test/unit/basic.fail.generate.test.js @@ -17,4 +17,14 @@ describe('basic fail generate', () => { expect(e.message).toBe('Not today!') }) }) + + test('Fail when generate.dir equals rootDir', async () => { + const options = await loadFixture('basic', { + generate: { dir: '../basic' } + }) + + expect(() => { + new Nuxt(options) /* eslint-disable-line no-new */ + }).toThrow(/options.generate.dir cannot be/) + }) }) diff --git a/test/unit/utils.test.js b/test/unit/utils.test.js index a3b2c63375..197c4ac9fe 100644 --- a/test/unit/utils.test.js +++ b/test/unit/utils.test.js @@ -236,6 +236,53 @@ describe('utils', () => { .toBe(Utils.wp(`loader1!loader2!..${path.sep}baz`)) }) }) + + describe('guardDir', () => { + test('Parent dir is guarded', () => { + expect(() => { + Utils.guardDir({ + dir1: '/root/parent', + dir2: '/root' + }, 'dir1', 'dir2') + }).toThrow() + }) + + test('Same dir is guarded', () => { + expect(() => { + Utils.guardDir({ + dir1: '/root/parent', + dir2: '/root/parent' + }, 'dir1', 'dir2') + }).toThrow() + }) + + test('Same level dir is not guarded', () => { + expect(() => { + Utils.guardDir({ + dir1: '/root/parent-next', + dir2: '/root/parent' + }, 'dir1', 'dir2') + }).not.toThrow() + }) + + test('Same level dir is not guarded 2', () => { + expect(() => { + Utils.guardDir({ + dir1: '/root/parent', + dir2: '/root/parent-next' + }, 'dir1', 'dir2') + }).not.toThrow() + }) + + test('Child dir is not guarded', () => { + expect(() => { + Utils.guardDir({ + dir1: '/root/parent', + dir2: '/root/parent/child' + }, 'dir1', 'dir2') + }).not.toThrow() + }) + }) }) test('createRoutes should allow snake case routes', () => {