mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-23 14:15:13 +00:00
feat(cli): lock project during build or generate (#4985)
This commit is contained in:
parent
45ad1f5250
commit
4e51723efc
@ -1,4 +1,4 @@
|
|||||||
|
import consola from 'consola'
|
||||||
import minimist from 'minimist'
|
import minimist from 'minimist'
|
||||||
import { name, version } from '../package.json'
|
import { name, version } from '../package.json'
|
||||||
import { loadNuxtConfig, forceExit } from './utils'
|
import { loadNuxtConfig, forceExit } from './utils'
|
||||||
@ -45,6 +45,10 @@ export default class NuxtCommand {
|
|||||||
|
|
||||||
const runResolve = Promise.resolve(this.cmd.run(this))
|
const runResolve = Promise.resolve(this.cmd.run(this))
|
||||||
|
|
||||||
|
if (this.argv.lock) {
|
||||||
|
runResolve.then(() => this.releaseLock())
|
||||||
|
}
|
||||||
|
|
||||||
if (this.argv['force-exit']) {
|
if (this.argv['force-exit']) {
|
||||||
const forceExitByUser = this.isUserSuppliedArg('force-exit')
|
const forceExitByUser = this.isUserSuppliedArg('force-exit')
|
||||||
runResolve.then(() => forceExit(this.cmd.name, forceExitByUser ? false : forceExitTimeout))
|
runResolve.then(() => forceExit(this.cmd.name, forceExitByUser ? false : forceExitTimeout))
|
||||||
@ -71,7 +75,7 @@ export default class NuxtCommand {
|
|||||||
|
|
||||||
async getNuxtConfig(extraOptions) {
|
async getNuxtConfig(extraOptions) {
|
||||||
const config = await loadNuxtConfig(this.argv)
|
const config = await loadNuxtConfig(this.argv)
|
||||||
const options = Object.assign(config, extraOptions || {})
|
const options = Object.assign(config, extraOptions)
|
||||||
|
|
||||||
for (const name of Object.keys(this.cmd.options)) {
|
for (const name of Object.keys(this.cmd.options)) {
|
||||||
this.cmd.options[name].prepare && this.cmd.options[name].prepare(this, options, this.argv)
|
this.cmd.options[name].prepare && this.cmd.options[name].prepare(this, options, this.argv)
|
||||||
@ -99,6 +103,26 @@ export default class NuxtCommand {
|
|||||||
return new Generator(nuxt, builder)
|
return new Generator(nuxt, builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setLock(lockRelease) {
|
||||||
|
if (lockRelease) {
|
||||||
|
if (this._lockRelease) {
|
||||||
|
consola.warn(`A previous unreleased lock was found, this shouldn't happen and is probably an error in 'nuxt ${this.cmd.name}' command. The lock will be removed but be aware of potential strange results`)
|
||||||
|
|
||||||
|
await this.releaseLock()
|
||||||
|
this._lockRelease = lockRelease
|
||||||
|
} else {
|
||||||
|
this._lockRelease = lockRelease
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseLock() {
|
||||||
|
if (this._lockRelease) {
|
||||||
|
await this._lockRelease()
|
||||||
|
this._lockRelease = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isUserSuppliedArg(option) {
|
isUserSuppliedArg(option) {
|
||||||
return this._argv.includes(`--${option}`) || this._argv.includes(`--no-${option}`)
|
return this._argv.includes(`--${option}`) || this._argv.includes(`--no-${option}`)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { common } from '../options'
|
import { common, locking } from '../options'
|
||||||
|
import { createLock } from '../utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'build',
|
name: 'build',
|
||||||
@ -6,6 +7,7 @@ export default {
|
|||||||
usage: 'build <dir>',
|
usage: 'build <dir>',
|
||||||
options: {
|
options: {
|
||||||
...common,
|
...common,
|
||||||
|
...locking,
|
||||||
analyze: {
|
analyze: {
|
||||||
alias: 'a',
|
alias: 'a',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@ -62,6 +64,14 @@ export default {
|
|||||||
const config = await cmd.getNuxtConfig({ dev: false })
|
const config = await cmd.getNuxtConfig({ dev: false })
|
||||||
const nuxt = await cmd.getNuxt(config)
|
const nuxt = await cmd.getNuxt(config)
|
||||||
|
|
||||||
|
if (cmd.argv.lock) {
|
||||||
|
await cmd.setLock(await createLock({
|
||||||
|
id: 'build',
|
||||||
|
dir: nuxt.options.buildDir,
|
||||||
|
root: config.rootDir
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
if (nuxt.options.mode !== 'spa' || cmd.argv.generate === false) {
|
if (nuxt.options.mode !== 'spa' || cmd.argv.generate === false) {
|
||||||
// Build only
|
// Build only
|
||||||
const builder = await cmd.getBuilder(nuxt)
|
const builder = await cmd.getBuilder(nuxt)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { common } from '../options'
|
import { common, locking } from '../options'
|
||||||
import { normalizeArg } from '../utils'
|
import { normalizeArg, createLock } from '../utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'generate',
|
name: 'generate',
|
||||||
@ -7,6 +7,7 @@ export default {
|
|||||||
usage: 'generate <dir>',
|
usage: 'generate <dir>',
|
||||||
options: {
|
options: {
|
||||||
...common,
|
...common,
|
||||||
|
...locking,
|
||||||
build: {
|
build: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: true,
|
default: true,
|
||||||
@ -44,6 +45,25 @@ export default {
|
|||||||
config.build.analyze = false
|
config.build.analyze = false
|
||||||
|
|
||||||
const nuxt = await cmd.getNuxt(config)
|
const nuxt = await cmd.getNuxt(config)
|
||||||
|
|
||||||
|
if (cmd.argv.lock) {
|
||||||
|
await cmd.setLock(await createLock({
|
||||||
|
id: 'build',
|
||||||
|
dir: nuxt.options.buildDir,
|
||||||
|
root: config.rootDir
|
||||||
|
}))
|
||||||
|
|
||||||
|
nuxt.hook('build:done', async () => {
|
||||||
|
await cmd.releaseLock()
|
||||||
|
|
||||||
|
await cmd.setLock(await createLock({
|
||||||
|
id: 'generate',
|
||||||
|
dir: nuxt.options.generate.dir,
|
||||||
|
root: config.rootDir
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const generator = await cmd.getGenerator(nuxt)
|
const generator = await cmd.getGenerator(nuxt)
|
||||||
|
|
||||||
await generator.generate({
|
await generator.generate({
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export { default as common } from './common'
|
export { default as common } from './common'
|
||||||
export { default as server } from './server'
|
export { default as server } from './server'
|
||||||
|
export { default as locking } from './locking'
|
||||||
|
7
packages/cli/src/options/locking.js
Normal file
7
packages/cli/src/options/locking.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
lock: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
description: 'Do not set a lock on the project when building'
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import esm from 'esm'
|
|||||||
import exit from 'exit'
|
import exit from 'exit'
|
||||||
import defaultsDeep from 'lodash/defaultsDeep'
|
import defaultsDeep from 'lodash/defaultsDeep'
|
||||||
import { defaultNuxtConfigFile, getDefaultNuxtConfig } from '@nuxt/config'
|
import { defaultNuxtConfigFile, getDefaultNuxtConfig } from '@nuxt/config'
|
||||||
|
import { lock } from '@nuxt/utils'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
import env from 'std-env'
|
import env from 'std-env'
|
||||||
@ -158,3 +159,9 @@ ${chalk.bold('DeprecationWarning: Starting with Nuxt version 3 this will be a fa
|
|||||||
exit(0)
|
exit(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An immediate export throws an error when mocking with jest
|
||||||
|
// TypeError: Cannot set property createLock of #<Object> which has only a getter
|
||||||
|
export function createLock(...args) {
|
||||||
|
return lock(...args)
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as utils from '../../src/utils/'
|
import * as utils from '../../src/utils'
|
||||||
import { mockGetNuxt, mockGetBuilder, mockGetGenerator, NuxtCommand } from '../utils'
|
import { mockGetNuxt, mockGetBuilder, mockGetGenerator, NuxtCommand } from '../utils'
|
||||||
|
|
||||||
describe('build', () => {
|
describe('build', () => {
|
||||||
@ -8,6 +8,7 @@ describe('build', () => {
|
|||||||
build = await import('../../src/commands/build').then(m => m.default)
|
build = await import('../../src/commands/build').then(m => m.default)
|
||||||
jest.spyOn(process, 'exit').mockImplementation(code => code)
|
jest.spyOn(process, 'exit').mockImplementation(code => code)
|
||||||
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
|
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
|
||||||
|
jest.spyOn(utils, 'createLock').mockImplementation(() => () => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => jest.resetAllMocks())
|
afterEach(() => jest.resetAllMocks())
|
||||||
@ -37,7 +38,7 @@ describe('build', () => {
|
|||||||
analyze: false
|
analyze: false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const generate = mockGetGenerator(Promise.resolve())
|
const generate = mockGetGenerator()
|
||||||
|
|
||||||
await NuxtCommand.from(build).run()
|
await NuxtCommand.from(build).run()
|
||||||
|
|
||||||
@ -106,4 +107,36 @@ describe('build', () => {
|
|||||||
|
|
||||||
expect(utils.forceExit).not.toHaveBeenCalled()
|
expect(utils.forceExit).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('build locks project by default', async () => {
|
||||||
|
mockGetNuxt({
|
||||||
|
mode: 'universal'
|
||||||
|
})
|
||||||
|
mockGetBuilder(Promise.resolve())
|
||||||
|
|
||||||
|
const releaseLock = jest.fn(() => Promise.resolve())
|
||||||
|
const createLock = jest.fn(() => releaseLock)
|
||||||
|
jest.spyOn(utils, 'createLock').mockImplementation(createLock)
|
||||||
|
|
||||||
|
const cmd = NuxtCommand.from(build, ['build', '.'])
|
||||||
|
await cmd.run()
|
||||||
|
|
||||||
|
expect(createLock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(releaseLock).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('build can disable locking', async () => {
|
||||||
|
mockGetNuxt({
|
||||||
|
mode: 'universal'
|
||||||
|
})
|
||||||
|
mockGetBuilder(Promise.resolve())
|
||||||
|
|
||||||
|
const createLock = jest.fn(() => Promise.resolve())
|
||||||
|
jest.spyOn(utils, 'createLock').mockImplementationOnce(() => createLock)
|
||||||
|
|
||||||
|
const cmd = NuxtCommand.from(build, ['build', '.', '--no-lock'])
|
||||||
|
await cmd.run()
|
||||||
|
|
||||||
|
expect(createLock).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -128,4 +128,29 @@ describe('cli/command', () => {
|
|||||||
expect(process.stdout.write).toHaveBeenCalled()
|
expect(process.stdout.write).toHaveBeenCalled()
|
||||||
process.stdout.write.mockRestore()
|
process.stdout.write.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('can set and release lock', () => {
|
||||||
|
const release = jest.fn(() => Promise.resolve())
|
||||||
|
const cmd = new Command()
|
||||||
|
|
||||||
|
cmd.setLock(release)
|
||||||
|
cmd.releaseLock()
|
||||||
|
|
||||||
|
expect(release).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('logs warning when lock already exists and removes old lock', () => {
|
||||||
|
const release = jest.fn(() => Promise.resolve())
|
||||||
|
const cmd = new Command()
|
||||||
|
|
||||||
|
cmd.setLock(release)
|
||||||
|
cmd.setLock(release)
|
||||||
|
|
||||||
|
expect(consola.warn).toHaveBeenCalledTimes(1)
|
||||||
|
expect(consola.warn).toHaveBeenCalledWith(expect.stringMatching('A previous unreleased lock was found'))
|
||||||
|
expect(release).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
cmd.releaseLock()
|
||||||
|
expect(release).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as utils from '../../src/utils/'
|
import * as utils from '../../src/utils'
|
||||||
import { mockGetNuxt, mockGetGenerator, NuxtCommand } from '../utils'
|
import { mockGetNuxt, mockGetGenerator, NuxtCommand } from '../utils'
|
||||||
|
|
||||||
describe('generate', () => {
|
describe('generate', () => {
|
||||||
@ -8,6 +8,7 @@ describe('generate', () => {
|
|||||||
generate = await import('../../src/commands/generate').then(m => m.default)
|
generate = await import('../../src/commands/generate').then(m => m.default)
|
||||||
jest.spyOn(process, 'exit').mockImplementation(code => code)
|
jest.spyOn(process, 'exit').mockImplementation(code => code)
|
||||||
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
|
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
|
||||||
|
jest.spyOn(utils, 'createLock').mockImplementation(() => () => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => jest.resetAllMocks())
|
afterEach(() => jest.resetAllMocks())
|
||||||
@ -18,7 +19,7 @@ describe('generate', () => {
|
|||||||
|
|
||||||
test('builds by default', async () => {
|
test('builds by default', async () => {
|
||||||
mockGetNuxt()
|
mockGetNuxt()
|
||||||
const generator = mockGetGenerator(Promise.resolve())
|
const generator = mockGetGenerator()
|
||||||
|
|
||||||
await NuxtCommand.from(generate).run()
|
await NuxtCommand.from(generate).run()
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ describe('generate', () => {
|
|||||||
|
|
||||||
test('doesnt build with no-build', async () => {
|
test('doesnt build with no-build', async () => {
|
||||||
mockGetNuxt()
|
mockGetNuxt()
|
||||||
const generator = mockGetGenerator(Promise.resolve())
|
const generator = mockGetGenerator()
|
||||||
|
|
||||||
await NuxtCommand.run(generate, ['generate', '.', '--no-build'])
|
await NuxtCommand.run(generate, ['generate', '.', '--no-build'])
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ describe('generate', () => {
|
|||||||
|
|
||||||
test('build with devtools', async () => {
|
test('build with devtools', async () => {
|
||||||
mockGetNuxt()
|
mockGetNuxt()
|
||||||
const generator = mockGetGenerator(Promise.resolve())
|
const generator = mockGetGenerator()
|
||||||
|
|
||||||
const cmd = NuxtCommand.from(generate, ['generate', '.', '--devtools'])
|
const cmd = NuxtCommand.from(generate, ['generate', '.', '--devtools'])
|
||||||
|
|
||||||
@ -53,20 +54,7 @@ describe('generate', () => {
|
|||||||
|
|
||||||
test('generate with modern mode', async () => {
|
test('generate with modern mode', async () => {
|
||||||
mockGetNuxt()
|
mockGetNuxt()
|
||||||
mockGetGenerator(Promise.resolve())
|
mockGetGenerator()
|
||||||
|
|
||||||
const cmd = NuxtCommand.from(generate, ['generate', '.', '--m'])
|
|
||||||
|
|
||||||
const options = await cmd.getNuxtConfig()
|
|
||||||
|
|
||||||
await cmd.run()
|
|
||||||
|
|
||||||
expect(options.modern).toBe('client')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('generate with modern mode', async () => {
|
|
||||||
mockGetNuxt()
|
|
||||||
mockGetGenerator(Promise.resolve())
|
|
||||||
|
|
||||||
const cmd = NuxtCommand.from(generate, ['generate', '.', '--m'])
|
const cmd = NuxtCommand.from(generate, ['generate', '.', '--m'])
|
||||||
|
|
||||||
@ -79,7 +67,7 @@ describe('generate', () => {
|
|||||||
|
|
||||||
test('generate force-exits by default', async () => {
|
test('generate force-exits by default', async () => {
|
||||||
mockGetNuxt()
|
mockGetNuxt()
|
||||||
mockGetGenerator(Promise.resolve())
|
mockGetGenerator()
|
||||||
|
|
||||||
const cmd = NuxtCommand.from(generate, ['generate', '.'])
|
const cmd = NuxtCommand.from(generate, ['generate', '.'])
|
||||||
await cmd.run()
|
await cmd.run()
|
||||||
@ -90,7 +78,7 @@ describe('generate', () => {
|
|||||||
|
|
||||||
test('generate can set force exit explicitly', async () => {
|
test('generate can set force exit explicitly', async () => {
|
||||||
mockGetNuxt()
|
mockGetNuxt()
|
||||||
mockGetGenerator(Promise.resolve())
|
mockGetGenerator()
|
||||||
|
|
||||||
const cmd = NuxtCommand.from(generate, ['generate', '.', '--force-exit'])
|
const cmd = NuxtCommand.from(generate, ['generate', '.', '--force-exit'])
|
||||||
await cmd.run()
|
await cmd.run()
|
||||||
@ -101,11 +89,45 @@ describe('generate', () => {
|
|||||||
|
|
||||||
test('generate can disable force exit explicitly', async () => {
|
test('generate can disable force exit explicitly', async () => {
|
||||||
mockGetNuxt()
|
mockGetNuxt()
|
||||||
mockGetGenerator(Promise.resolve())
|
mockGetGenerator()
|
||||||
|
|
||||||
const cmd = NuxtCommand.from(generate, ['generate', '.', '--no-force-exit'])
|
const cmd = NuxtCommand.from(generate, ['generate', '.', '--no-force-exit'])
|
||||||
await cmd.run()
|
await cmd.run()
|
||||||
|
|
||||||
expect(utils.forceExit).not.toHaveBeenCalled()
|
expect(utils.forceExit).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('generate locks project by default twice', async () => {
|
||||||
|
const releaseLock = jest.fn(() => Promise.resolve())
|
||||||
|
const createLock = jest.fn(() => releaseLock)
|
||||||
|
jest.spyOn(utils, 'createLock').mockImplementation(createLock)
|
||||||
|
|
||||||
|
let buildDone
|
||||||
|
mockGetNuxt({ generate: {} }, {
|
||||||
|
hook: (hookName, fn) => (buildDone = fn)
|
||||||
|
})
|
||||||
|
|
||||||
|
mockGetGenerator(async () => {
|
||||||
|
await buildDone()
|
||||||
|
})
|
||||||
|
|
||||||
|
const cmd = NuxtCommand.from(generate, ['generate', '.'])
|
||||||
|
await cmd.run()
|
||||||
|
|
||||||
|
expect(createLock).toHaveBeenCalledTimes(2)
|
||||||
|
expect(releaseLock).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('generate can disable locking', async () => {
|
||||||
|
mockGetNuxt()
|
||||||
|
mockGetGenerator()
|
||||||
|
|
||||||
|
const createLock = jest.fn(() => Promise.resolve())
|
||||||
|
jest.spyOn(utils, 'createLock').mockImplementationOnce(() => createLock)
|
||||||
|
|
||||||
|
const cmd = NuxtCommand.from(generate, ['generate', '.', '--no-lock'])
|
||||||
|
await cmd.run()
|
||||||
|
|
||||||
|
expect(createLock).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
31
packages/cli/test/unit/utils-minimalcli.test.js
Normal file
31
packages/cli/test/unit/utils-minimalcli.test.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { consola } from '../utils'
|
||||||
|
import * as utils from '../../src/utils'
|
||||||
|
|
||||||
|
jest.mock('std-env', () => ({
|
||||||
|
test: false,
|
||||||
|
minimalCLI: true
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('cli/utils', () => {
|
||||||
|
afterEach(() => jest.resetAllMocks())
|
||||||
|
|
||||||
|
test('showBanner prints only listeners', () => {
|
||||||
|
const listeners = [
|
||||||
|
{ url: 'first' },
|
||||||
|
{ url: 'second' }
|
||||||
|
]
|
||||||
|
|
||||||
|
utils.showBanner({
|
||||||
|
options: {
|
||||||
|
cli: {}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
listeners
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(consola.info).toHaveBeenCalledTimes(2)
|
||||||
|
expect(consola.info).toHaveBeenCalledWith(`Listening on: ${listeners[0].url}`)
|
||||||
|
expect(consola.info).toHaveBeenCalledWith(`Listening on: ${listeners[1].url}`)
|
||||||
|
})
|
||||||
|
})
|
@ -3,6 +3,11 @@ import { consola } from '../utils'
|
|||||||
import * as utils from '../../src/utils'
|
import * as utils from '../../src/utils'
|
||||||
import * as fmt from '../../src/utils/formatting'
|
import * as fmt from '../../src/utils/formatting'
|
||||||
|
|
||||||
|
jest.mock('std-env', () => ({
|
||||||
|
test: false,
|
||||||
|
minimalCLI: false
|
||||||
|
}))
|
||||||
|
|
||||||
describe('cli/utils', () => {
|
describe('cli/utils', () => {
|
||||||
afterEach(() => jest.resetAllMocks())
|
afterEach(() => jest.resetAllMocks())
|
||||||
|
|
||||||
@ -113,4 +118,67 @@ describe('cli/utils', () => {
|
|||||||
test('indent custom char', () => {
|
test('indent custom char', () => {
|
||||||
expect(fmt.indent(4, '-')).toBe('----')
|
expect(fmt.indent(4, '-')).toBe('----')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('showBanner prints full-info box', () => {
|
||||||
|
const stdout = jest.spyOn(process.stdout, 'write').mockImplementation(() => {})
|
||||||
|
const successBox = jest.fn().mockImplementation((m, t) => t + m)
|
||||||
|
jest.spyOn(fmt, 'successBox').mockImplementation(successBox)
|
||||||
|
|
||||||
|
const badgeMessages = [ 'badgeMessage' ]
|
||||||
|
const listeners = [
|
||||||
|
{ url: 'first' },
|
||||||
|
{ url: 'second' }
|
||||||
|
]
|
||||||
|
|
||||||
|
utils.showBanner({
|
||||||
|
options: {
|
||||||
|
cli: {
|
||||||
|
badgeMessages
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
listeners
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(successBox).toHaveBeenCalledTimes(1)
|
||||||
|
expect(stdout).toHaveBeenCalledTimes(1)
|
||||||
|
expect(stdout).toHaveBeenCalledWith(expect.stringMatching('Nuxt.js'))
|
||||||
|
expect(stdout).toHaveBeenCalledWith(expect.stringMatching(`Listening on: ${listeners[0].url}`))
|
||||||
|
expect(stdout).toHaveBeenCalledWith(expect.stringMatching(`Listening on: ${listeners[1].url}`))
|
||||||
|
expect(stdout).toHaveBeenCalledWith(expect.stringMatching('badgeMessage'))
|
||||||
|
stdout.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('forceExit exits after timeout', () => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
const exit = jest.spyOn(process, 'exit').mockImplementation(() => {})
|
||||||
|
const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => {})
|
||||||
|
|
||||||
|
utils.forceExit('test', 1)
|
||||||
|
expect(exit).not.toHaveBeenCalled()
|
||||||
|
jest.runAllTimers()
|
||||||
|
|
||||||
|
expect(stderr).toHaveBeenCalledTimes(1)
|
||||||
|
expect(stderr).toHaveBeenCalledWith(expect.stringMatching('Nuxt.js will now force exit'))
|
||||||
|
expect(exit).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
stderr.mockRestore()
|
||||||
|
exit.mockRestore()
|
||||||
|
jest.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('forceExit exits immediately without timeout', () => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
const exit = jest.spyOn(process, 'exit').mockImplementation(() => {})
|
||||||
|
const stderr = jest.spyOn(process.stderr, 'write').mockImplementation(() => {})
|
||||||
|
|
||||||
|
utils.forceExit('test', false)
|
||||||
|
expect(stderr).not.toHaveBeenCalledWith()
|
||||||
|
expect(exit).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
stderr.mockRestore()
|
||||||
|
exit.mockRestore()
|
||||||
|
jest.useRealTimers()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -28,13 +28,9 @@ export const mockGetNuxt = (options = {}, implementation) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const mockGetBuilder = (ret) => {
|
export const mockGetBuilder = (ret) => {
|
||||||
const build = jest.fn().mockImplementationOnce(() => {
|
const build = jest.fn().mockImplementationOnce(() => ret)
|
||||||
return ret
|
|
||||||
})
|
|
||||||
|
|
||||||
Command.prototype.getBuilder = jest.fn().mockImplementationOnce(() => {
|
Command.prototype.getBuilder = jest.fn().mockImplementationOnce(() => ({ build }))
|
||||||
return { build }
|
|
||||||
})
|
|
||||||
|
|
||||||
return build
|
return build
|
||||||
}
|
}
|
||||||
@ -42,14 +38,10 @@ export const mockGetBuilder = (ret) => {
|
|||||||
export const mockGetGenerator = (ret) => {
|
export const mockGetGenerator = (ret) => {
|
||||||
const generate = jest.fn()
|
const generate = jest.fn()
|
||||||
if (ret) {
|
if (ret) {
|
||||||
generate.mockImplementationOnce(() => {
|
generate.mockImplementationOnce(ret)
|
||||||
return ret
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Command.prototype.getGenerator = jest.fn().mockImplementationOnce(() => {
|
Command.prototype.getGenerator = jest.fn().mockImplementationOnce(() => ({ generate }))
|
||||||
return { generate }
|
|
||||||
})
|
|
||||||
|
|
||||||
return generate
|
return generate
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,8 @@ import { chainFn } from '@nuxt/utils'
|
|||||||
import ModuleContainer from '../src/module'
|
import ModuleContainer from '../src/module'
|
||||||
|
|
||||||
jest.mock('fs', () => ({
|
jest.mock('fs', () => ({
|
||||||
existsSync: Boolean
|
existsSync: Boolean,
|
||||||
|
closeSync: Boolean
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('hash-sum', () => src => `hash(${src})`)
|
jest.mock('hash-sum', () => src => `hash(${src})`)
|
||||||
|
@ -9,7 +9,10 @@
|
|||||||
"main": "dist/utils.js",
|
"main": "dist/utils.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"consola": "^2.5.6",
|
"consola": "^2.5.6",
|
||||||
"serialize-javascript": "^1.6.1"
|
"hash-sum": "^1.0.2",
|
||||||
|
"proper-lockfile": "^3.2.0",
|
||||||
|
"serialize-javascript": "^1.6.1",
|
||||||
|
"signal-exit": "^3.0.2"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export * from './context'
|
export * from './context'
|
||||||
export * from './lang'
|
export * from './lang'
|
||||||
|
export * from './locking'
|
||||||
export * from './resolve'
|
export * from './resolve'
|
||||||
export * from './route'
|
export * from './route'
|
||||||
export * from './serialize'
|
export * from './serialize'
|
||||||
|
65
packages/utils/src/locking.js
Normal file
65
packages/utils/src/locking.js
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import path from 'path'
|
||||||
|
import consola from 'consola'
|
||||||
|
import hash from 'hash-sum'
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import properlock from 'proper-lockfile'
|
||||||
|
import onExit from 'signal-exit'
|
||||||
|
|
||||||
|
export const lockPaths = new Set()
|
||||||
|
|
||||||
|
export const defaultLockOptions = {
|
||||||
|
stale: 15000,
|
||||||
|
onCompromised: err => consola.fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLockOptions(options) {
|
||||||
|
return Object.assign({}, defaultLockOptions, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createLockPath({ id = 'nuxt', dir, root }) {
|
||||||
|
const sum = hash(`${root}-${dir}`)
|
||||||
|
|
||||||
|
return path.resolve(root, 'node_modules/.cache/nuxt', `${id}-lock-${sum}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLockPath(config) {
|
||||||
|
const lockPath = createLockPath(config)
|
||||||
|
|
||||||
|
// the lock is created for the lockPath as ${lockPath}.lock
|
||||||
|
// so the (temporary) lockPath needs to exist
|
||||||
|
await fs.ensureDir(lockPath)
|
||||||
|
|
||||||
|
return lockPath
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function lock({ id, dir, root, options }) {
|
||||||
|
const lockPath = await getLockPath({ id, dir, root })
|
||||||
|
|
||||||
|
const locked = await properlock.check(lockPath)
|
||||||
|
if (locked) {
|
||||||
|
consola.fatal(`A lock with id '${id}' already exists on ${dir}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const release = await properlock.lock(lockPath, options)
|
||||||
|
|
||||||
|
if (!release) {
|
||||||
|
consola.warn(`Unable to get a lock with id '${id}' on ${dir} (but will continue)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lockPaths.size) {
|
||||||
|
// make sure to always cleanup our temporate lockPaths
|
||||||
|
onExit(() => {
|
||||||
|
for (const lockPath of lockPaths) {
|
||||||
|
fs.removeSync(lockPath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
lockPaths.add(lockPath)
|
||||||
|
|
||||||
|
return async function lockRelease() {
|
||||||
|
await release()
|
||||||
|
await fs.remove(lockPath)
|
||||||
|
lockPaths.delete(lockPath)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import * as Util from '../src'
|
import * as Util from '../src'
|
||||||
import * as context from '../src/context'
|
import * as context from '../src/context'
|
||||||
import * as lang from '../src/lang'
|
import * as lang from '../src/lang'
|
||||||
|
import * as locking from '../src/locking'
|
||||||
import * as resolve from '../src/resolve'
|
import * as resolve from '../src/resolve'
|
||||||
import * as route from '../src/route'
|
import * as route from '../src/route'
|
||||||
import * as serialize from '../src/serialize'
|
import * as serialize from '../src/serialize'
|
||||||
@ -12,6 +13,7 @@ describe('util: entry', () => {
|
|||||||
expect(Util).toEqual({
|
expect(Util).toEqual({
|
||||||
...context,
|
...context,
|
||||||
...lang,
|
...lang,
|
||||||
|
...locking,
|
||||||
...resolve,
|
...resolve,
|
||||||
...route,
|
...route,
|
||||||
...serialize,
|
...serialize,
|
||||||
|
146
packages/utils/test/locking.test.js
Normal file
146
packages/utils/test/locking.test.js
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
import consola from 'consola'
|
||||||
|
import fs from 'fs-extra'
|
||||||
|
import properlock from 'proper-lockfile'
|
||||||
|
import onExit from 'signal-exit'
|
||||||
|
import { lockPaths, defaultLockOptions, getLockOptions, createLockPath, getLockPath, lock } from '../src/locking'
|
||||||
|
|
||||||
|
jest.mock('fs-extra')
|
||||||
|
jest.mock('proper-lockfile')
|
||||||
|
jest.mock('signal-exit')
|
||||||
|
|
||||||
|
describe('util: locking', () => {
|
||||||
|
const lockConfig = {
|
||||||
|
id: 'id',
|
||||||
|
dir: 'dist',
|
||||||
|
root: '/project-root'
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => jest.resetAllMocks())
|
||||||
|
beforeEach(() => lockPaths.clear())
|
||||||
|
|
||||||
|
test('onCompromised lock is fatal error by default', () => {
|
||||||
|
defaultLockOptions.onCompromised()
|
||||||
|
expect(consola.fatal).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('can override default options', () => {
|
||||||
|
const options = getLockOptions({ onCompromised: err => consola.warn(err) })
|
||||||
|
options.onCompromised()
|
||||||
|
|
||||||
|
expect(consola.warn).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createLockPath creates the same lockPath for identical locks', () => {
|
||||||
|
const path1 = createLockPath(lockConfig)
|
||||||
|
const path2 = createLockPath(Object.assign({}, lockConfig))
|
||||||
|
expect(path1).toBe(path2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createLockPath creates unique lockPaths for different ids', () => {
|
||||||
|
const path1 = createLockPath(lockConfig)
|
||||||
|
const path2 = createLockPath(Object.assign({}, lockConfig, { id: 'id2' }))
|
||||||
|
expect(path1).not.toBe(path2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createLockPath creates unique lockPaths for different dirs', () => {
|
||||||
|
const path1 = createLockPath(lockConfig)
|
||||||
|
const path2 = createLockPath(Object.assign({}, lockConfig, { dir: 'dir2' }))
|
||||||
|
expect(path1).not.toBe(path2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('createLockPath creates unique lockPaths for different roots', () => {
|
||||||
|
const path1 = createLockPath(lockConfig)
|
||||||
|
const path2 = createLockPath(Object.assign({}, lockConfig, { root: '/project-root2' }))
|
||||||
|
expect(path1).not.toBe(path2)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('getLockPath creates lockPath when it doesnt exists', () => {
|
||||||
|
getLockPath(lockConfig)
|
||||||
|
|
||||||
|
expect(fs.ensureDir).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('lock creates a lock and returns a release fn', async () => {
|
||||||
|
properlock.lock.mockImplementationOnce(() => true)
|
||||||
|
|
||||||
|
const fn = await lock(lockConfig)
|
||||||
|
|
||||||
|
expect(properlock.check).toHaveBeenCalledTimes(1)
|
||||||
|
expect(properlock.lock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fs.ensureDir).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fn).toEqual(expect.any(Function))
|
||||||
|
expect(consola.error).not.toHaveBeenCalled()
|
||||||
|
expect(consola.fatal).not.toHaveBeenCalled()
|
||||||
|
expect(consola.warn).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('lock throws error when lock already exists', async () => {
|
||||||
|
properlock.check.mockImplementationOnce(() => true)
|
||||||
|
|
||||||
|
await lock(lockConfig)
|
||||||
|
expect(properlock.check).toHaveBeenCalledTimes(1)
|
||||||
|
expect(consola.fatal).toHaveBeenCalledTimes(1)
|
||||||
|
expect(consola.fatal).toHaveBeenCalledWith(`A lock with id '${lockConfig.id}' already exists on ${lockConfig.dir}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('lock logs warning when it couldnt get a lock', async () => {
|
||||||
|
properlock.lock.mockImplementationOnce(() => false)
|
||||||
|
|
||||||
|
await lock(lockConfig)
|
||||||
|
expect(properlock.lock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(consola.warn).toHaveBeenCalledTimes(1)
|
||||||
|
expect(consola.warn).toHaveBeenCalledWith(`Unable to get a lock with id '${lockConfig.id}' on ${lockConfig.dir} (but will continue)`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('lock returns a release method for unlocking both lockfile as lockPath', async () => {
|
||||||
|
const release = jest.fn()
|
||||||
|
properlock.lock.mockImplementationOnce(() => release)
|
||||||
|
|
||||||
|
const fn = await lock(lockConfig)
|
||||||
|
await fn()
|
||||||
|
|
||||||
|
expect(release).toHaveBeenCalledTimes(1)
|
||||||
|
expect(fs.remove).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('lock release also cleansup onExit set', async () => {
|
||||||
|
const release = jest.fn()
|
||||||
|
properlock.lock.mockImplementationOnce(() => release)
|
||||||
|
|
||||||
|
const fn = await lock(lockConfig)
|
||||||
|
expect(lockPaths.size).toBe(1)
|
||||||
|
|
||||||
|
await fn()
|
||||||
|
expect(lockPaths.size).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('lock sets exit listener once to remove lockPaths', async () => {
|
||||||
|
properlock.lock.mockImplementationOnce(() => true)
|
||||||
|
|
||||||
|
await lock(lockConfig)
|
||||||
|
await lock(lockConfig)
|
||||||
|
|
||||||
|
expect(onExit).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('exit listener removes all lockPaths when called', async () => {
|
||||||
|
let callback
|
||||||
|
onExit.mockImplementationOnce(cb => (callback = cb))
|
||||||
|
|
||||||
|
const lockConfig2 = Object.assign({}, lockConfig, { id: 'id2' })
|
||||||
|
|
||||||
|
const path1 = createLockPath(lockConfig)
|
||||||
|
const path2 = createLockPath(lockConfig2)
|
||||||
|
|
||||||
|
await lock(lockConfig)
|
||||||
|
await lock(lockConfig2)
|
||||||
|
|
||||||
|
expect(onExit).toHaveBeenCalledTimes(1)
|
||||||
|
expect(lockPaths.size).toBe(2)
|
||||||
|
expect(callback).toBeDefined()
|
||||||
|
callback()
|
||||||
|
|
||||||
|
expect(fs.removeSync).toHaveBeenCalledWith(path1)
|
||||||
|
expect(fs.removeSync).toHaveBeenCalledWith(path2)
|
||||||
|
})
|
||||||
|
})
|
14
yarn.lock
14
yarn.lock
@ -8594,6 +8594,15 @@ promzard@^0.3.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
read "1"
|
read "1"
|
||||||
|
|
||||||
|
proper-lockfile@^3.2.0:
|
||||||
|
version "3.2.0"
|
||||||
|
resolved "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-3.2.0.tgz#89ca420eea1d55d38ca552578851460067bcda66"
|
||||||
|
integrity sha512-iMghHHXv2bsxl6NchhEaFck8tvX3F9cknEEh1SUpguUOBjN7PAAW9BLzmbc1g/mCD1gY3EE2EABBHPJfFdHFmA==
|
||||||
|
dependencies:
|
||||||
|
graceful-fs "^4.1.11"
|
||||||
|
retry "^0.12.0"
|
||||||
|
signal-exit "^3.0.2"
|
||||||
|
|
||||||
proto-list@~1.2.1:
|
proto-list@~1.2.1:
|
||||||
version "1.2.4"
|
version "1.2.4"
|
||||||
resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849"
|
||||||
@ -9288,6 +9297,11 @@ retry@^0.10.0:
|
|||||||
resolved "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
|
resolved "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4"
|
||||||
integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=
|
integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=
|
||||||
|
|
||||||
|
retry@^0.12.0:
|
||||||
|
version "0.12.0"
|
||||||
|
resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
|
||||||
|
integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
|
||||||
|
|
||||||
rgb-regex@^1.0.1:
|
rgb-regex@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
|
resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
|
||||||
|
Loading…
Reference in New Issue
Block a user