mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-27 08:02:01 +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 { name, version } from '../package.json'
|
||||
import { loadNuxtConfig, forceExit } from './utils'
|
||||
@ -45,6 +45,10 @@ export default class NuxtCommand {
|
||||
|
||||
const runResolve = Promise.resolve(this.cmd.run(this))
|
||||
|
||||
if (this.argv.lock) {
|
||||
runResolve.then(() => this.releaseLock())
|
||||
}
|
||||
|
||||
if (this.argv['force-exit']) {
|
||||
const forceExitByUser = this.isUserSuppliedArg('force-exit')
|
||||
runResolve.then(() => forceExit(this.cmd.name, forceExitByUser ? false : forceExitTimeout))
|
||||
@ -71,7 +75,7 @@ export default class NuxtCommand {
|
||||
|
||||
async getNuxtConfig(extraOptions) {
|
||||
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)) {
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
name: 'build',
|
||||
@ -6,6 +7,7 @@ export default {
|
||||
usage: 'build <dir>',
|
||||
options: {
|
||||
...common,
|
||||
...locking,
|
||||
analyze: {
|
||||
alias: 'a',
|
||||
type: 'boolean',
|
||||
@ -62,6 +64,14 @@ export default {
|
||||
const config = await cmd.getNuxtConfig({ dev: false })
|
||||
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) {
|
||||
// Build only
|
||||
const builder = await cmd.getBuilder(nuxt)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { common } from '../options'
|
||||
import { normalizeArg } from '../utils'
|
||||
import { common, locking } from '../options'
|
||||
import { normalizeArg, createLock } from '../utils'
|
||||
|
||||
export default {
|
||||
name: 'generate',
|
||||
@ -7,6 +7,7 @@ export default {
|
||||
usage: 'generate <dir>',
|
||||
options: {
|
||||
...common,
|
||||
...locking,
|
||||
build: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
@ -44,6 +45,25 @@ export default {
|
||||
config.build.analyze = false
|
||||
|
||||
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)
|
||||
|
||||
await generator.generate({
|
||||
|
@ -1,2 +1,3 @@
|
||||
export { default as common } from './common'
|
||||
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 defaultsDeep from 'lodash/defaultsDeep'
|
||||
import { defaultNuxtConfigFile, getDefaultNuxtConfig } from '@nuxt/config'
|
||||
import { lock } from '@nuxt/utils'
|
||||
import chalk from 'chalk'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
import env from 'std-env'
|
||||
@ -158,3 +159,9 @@ ${chalk.bold('DeprecationWarning: Starting with Nuxt version 3 this will be a fa
|
||||
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'
|
||||
|
||||
describe('build', () => {
|
||||
@ -8,6 +8,7 @@ describe('build', () => {
|
||||
build = await import('../../src/commands/build').then(m => m.default)
|
||||
jest.spyOn(process, 'exit').mockImplementation(code => code)
|
||||
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
|
||||
jest.spyOn(utils, 'createLock').mockImplementation(() => () => {})
|
||||
})
|
||||
|
||||
afterEach(() => jest.resetAllMocks())
|
||||
@ -37,7 +38,7 @@ describe('build', () => {
|
||||
analyze: false
|
||||
}
|
||||
})
|
||||
const generate = mockGetGenerator(Promise.resolve())
|
||||
const generate = mockGetGenerator()
|
||||
|
||||
await NuxtCommand.from(build).run()
|
||||
|
||||
@ -106,4 +107,36 @@ describe('build', () => {
|
||||
|
||||
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()
|
||||
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'
|
||||
|
||||
describe('generate', () => {
|
||||
@ -8,6 +8,7 @@ describe('generate', () => {
|
||||
generate = await import('../../src/commands/generate').then(m => m.default)
|
||||
jest.spyOn(process, 'exit').mockImplementation(code => code)
|
||||
jest.spyOn(utils, 'forceExit').mockImplementation(() => {})
|
||||
jest.spyOn(utils, 'createLock').mockImplementation(() => () => {})
|
||||
})
|
||||
|
||||
afterEach(() => jest.resetAllMocks())
|
||||
@ -18,7 +19,7 @@ describe('generate', () => {
|
||||
|
||||
test('builds by default', async () => {
|
||||
mockGetNuxt()
|
||||
const generator = mockGetGenerator(Promise.resolve())
|
||||
const generator = mockGetGenerator()
|
||||
|
||||
await NuxtCommand.from(generate).run()
|
||||
|
||||
@ -28,7 +29,7 @@ describe('generate', () => {
|
||||
|
||||
test('doesnt build with no-build', async () => {
|
||||
mockGetNuxt()
|
||||
const generator = mockGetGenerator(Promise.resolve())
|
||||
const generator = mockGetGenerator()
|
||||
|
||||
await NuxtCommand.run(generate, ['generate', '.', '--no-build'])
|
||||
|
||||
@ -38,7 +39,7 @@ describe('generate', () => {
|
||||
|
||||
test('build with devtools', async () => {
|
||||
mockGetNuxt()
|
||||
const generator = mockGetGenerator(Promise.resolve())
|
||||
const generator = mockGetGenerator()
|
||||
|
||||
const cmd = NuxtCommand.from(generate, ['generate', '.', '--devtools'])
|
||||
|
||||
@ -53,20 +54,7 @@ describe('generate', () => {
|
||||
|
||||
test('generate with modern mode', async () => {
|
||||
mockGetNuxt()
|
||||
mockGetGenerator(Promise.resolve())
|
||||
|
||||
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())
|
||||
mockGetGenerator()
|
||||
|
||||
const cmd = NuxtCommand.from(generate, ['generate', '.', '--m'])
|
||||
|
||||
@ -79,7 +67,7 @@ describe('generate', () => {
|
||||
|
||||
test('generate force-exits by default', async () => {
|
||||
mockGetNuxt()
|
||||
mockGetGenerator(Promise.resolve())
|
||||
mockGetGenerator()
|
||||
|
||||
const cmd = NuxtCommand.from(generate, ['generate', '.'])
|
||||
await cmd.run()
|
||||
@ -90,7 +78,7 @@ describe('generate', () => {
|
||||
|
||||
test('generate can set force exit explicitly', async () => {
|
||||
mockGetNuxt()
|
||||
mockGetGenerator(Promise.resolve())
|
||||
mockGetGenerator()
|
||||
|
||||
const cmd = NuxtCommand.from(generate, ['generate', '.', '--force-exit'])
|
||||
await cmd.run()
|
||||
@ -101,11 +89,45 @@ describe('generate', () => {
|
||||
|
||||
test('generate can disable force exit explicitly', async () => {
|
||||
mockGetNuxt()
|
||||
mockGetGenerator(Promise.resolve())
|
||||
mockGetGenerator()
|
||||
|
||||
const cmd = NuxtCommand.from(generate, ['generate', '.', '--no-force-exit'])
|
||||
await cmd.run()
|
||||
|
||||
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 fmt from '../../src/utils/formatting'
|
||||
|
||||
jest.mock('std-env', () => ({
|
||||
test: false,
|
||||
minimalCLI: false
|
||||
}))
|
||||
|
||||
describe('cli/utils', () => {
|
||||
afterEach(() => jest.resetAllMocks())
|
||||
|
||||
@ -113,4 +118,67 @@ describe('cli/utils', () => {
|
||||
test('indent custom char', () => {
|
||||
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) => {
|
||||
const build = jest.fn().mockImplementationOnce(() => {
|
||||
return ret
|
||||
})
|
||||
const build = jest.fn().mockImplementationOnce(() => ret)
|
||||
|
||||
Command.prototype.getBuilder = jest.fn().mockImplementationOnce(() => {
|
||||
return { build }
|
||||
})
|
||||
Command.prototype.getBuilder = jest.fn().mockImplementationOnce(() => ({ build }))
|
||||
|
||||
return build
|
||||
}
|
||||
@ -42,14 +38,10 @@ export const mockGetBuilder = (ret) => {
|
||||
export const mockGetGenerator = (ret) => {
|
||||
const generate = jest.fn()
|
||||
if (ret) {
|
||||
generate.mockImplementationOnce(() => {
|
||||
return ret
|
||||
})
|
||||
generate.mockImplementationOnce(ret)
|
||||
}
|
||||
|
||||
Command.prototype.getGenerator = jest.fn().mockImplementationOnce(() => {
|
||||
return { generate }
|
||||
})
|
||||
Command.prototype.getGenerator = jest.fn().mockImplementationOnce(() => ({ generate }))
|
||||
|
||||
return generate
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ import { chainFn } from '@nuxt/utils'
|
||||
import ModuleContainer from '../src/module'
|
||||
|
||||
jest.mock('fs', () => ({
|
||||
existsSync: Boolean
|
||||
existsSync: Boolean,
|
||||
closeSync: Boolean
|
||||
}))
|
||||
|
||||
jest.mock('hash-sum', () => src => `hash(${src})`)
|
||||
|
@ -9,7 +9,10 @@
|
||||
"main": "dist/utils.js",
|
||||
"dependencies": {
|
||||
"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": {
|
||||
"access": "public"
|
||||
|
@ -1,5 +1,6 @@
|
||||
export * from './context'
|
||||
export * from './lang'
|
||||
export * from './locking'
|
||||
export * from './resolve'
|
||||
export * from './route'
|
||||
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 context from '../src/context'
|
||||
import * as lang from '../src/lang'
|
||||
import * as locking from '../src/locking'
|
||||
import * as resolve from '../src/resolve'
|
||||
import * as route from '../src/route'
|
||||
import * as serialize from '../src/serialize'
|
||||
@ -12,6 +13,7 @@ describe('util: entry', () => {
|
||||
expect(Util).toEqual({
|
||||
...context,
|
||||
...lang,
|
||||
...locking,
|
||||
...resolve,
|
||||
...route,
|
||||
...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:
|
||||
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:
|
||||
version "1.2.4"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
|
||||
|
Loading…
Reference in New Issue
Block a user