feat(cli): lock project during build or generate (#4985)

This commit is contained in:
Pim 2019-03-03 09:12:46 +01:00 committed by Pooya Parsa
parent 45ad1f5250
commit 4e51723efc
19 changed files with 514 additions and 42 deletions

View File

@ -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}`)
}

View File

@ -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)

View File

@ -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({

View File

@ -1,2 +1,3 @@
export { default as common } from './common'
export { default as server } from './server'
export { default as locking } from './locking'

View File

@ -0,0 +1,7 @@
export default {
lock: {
type: 'boolean',
default: true,
description: 'Do not set a lock on the project when building'
}
}

View File

@ -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)
}

View File

@ -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()
})
})

View File

@ -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)
})
})

View File

@ -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()
})
})

View 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}`)
})
})

View File

@ -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()
})
})

View File

@ -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
}

View File

@ -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})`)

View File

@ -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"

View File

@ -1,5 +1,6 @@
export * from './context'
export * from './lang'
export * from './locking'
export * from './resolve'
export * from './route'
export * from './serialize'

View 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)
}
}

View File

@ -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,

View 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)
})
})

View File

@ -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"