Browse Source

refactor the `readMetaData()` function (#508)

* refactor the `readMetaData()` function

* add a couple new test cases

* test: restore "build" prefix

Fixes Node.js <= v6

* add default values for `npm`/`docker` get files functions

Makes the logic simpler, since we don't have to check for existence.

* throw an error when missing start/now-start or server.js

* pass in entire `readMetaData()` result to Now.create()

This avoids us reading these files from the filesystem again
master
Nathan Rajlich 8 years ago
committed by GitHub
parent
commit
49a2a645c8
  1. 258
      bin/now-deploy.js
  2. 10
      lib/get-files.js
  3. 5
      lib/git.js
  4. 86
      lib/index.js
  5. 237
      lib/read-metadata.js
  6. 1
      test/_fixtures/multiple-manifests-throws/Dockerfile
  7. 3
      test/_fixtures/multiple-manifests-throws/package.json
  8. 1
      test/_fixtures/now-json-docker/now.json
  9. 1
      test/_fixtures/type-in-package-now-with-dockerfile/Dockerfile
  10. 5
      test/_fixtures/type-in-package-now-with-dockerfile/package.json
  11. 26
      test/index.js

258
bin/now-deploy.js

@ -12,6 +12,7 @@ const minimist = require('minimist')
const ms = require('ms')
const flatten = require('arr-flatten')
const dotenv = require('dotenv')
const retry = require('async-retry')
const { eraseLines } = require('ansi-escapes')
const { write: copy } = require('clipboardy')
@ -167,6 +168,7 @@ const gitRepo = {}
// Options
let forceNew = argv.force
let deploymentName = argv.name
const debug = argv.debug
const clipboard = !argv['no-clipboard']
const forwardNpm = argv['forward-npm']
@ -174,7 +176,6 @@ const forceSync = argv.forceSync
const shouldLogin = argv.login
const followSymlinks = !argv.links
const wantsPublic = argv.public
const deploymentName = argv.name || false
const apiUrl = argv.url || 'https://api.zeit.co'
const isTTY = process.stdout.isTTY
const quiet = !isTTY
@ -191,51 +192,53 @@ if (Array.isArray(autoAliases)) {
console.log('Read more about the new way here: http://bit.ly/2l2v5Fg\n')
}
// Create a new deployment if user changed
// the name or made _src public.
// This should just work fine because it doesn't
// force a new sync, it just forces a new deployment.
const stopDeployment = msg => {
error(msg)
process.exit(1)
}
// Create a new deployment if user changed the name or made `_src` public.
// This works fine because it doesn't force a new sync,
// it just forces a new deployment.
if (deploymentName || wantsPublic) {
forceNew = true
}
let alwaysForwardNpm
Promise.resolve().then(async () => {
async function main() {
let config = await cfg.read({ token: argv.token })
alwaysForwardNpm = config.forwardNpm
if (argv.h || argv.help) {
help()
exit(0)
return exit(0)
} else if (argv.v || argv.version) {
console.log(version)
process.exit(0)
} else if (!config.token || shouldLogin) {
let token
return exit(0)
}
let token = argv.token || config.token
if (!token || shouldLogin) {
try {
token = await login(apiUrl)
config = await cfg.read()
} catch (err) {
error(`Authentication error – ${err.message}`)
process.exit(1)
return stopDeployment(`Authentication error – ${err.message}`)
}
if (shouldLogin) {
console.log('> Logged in successfully. Token saved in ~/.now.json')
process.exit(0)
} else {
sync({ token, config }).catch(err => {
error(`Unknown error: ${err}\n${err.stack}`)
process.exit(1)
})
return exit(0)
}
} else {
sync({ token: argv.token || config.token, config }).catch(err => {
error(`Unknown error: ${err}\n${err.stack}`)
process.exit(1)
})
}
})
// If we got to here then `token` should be set
try {
await sync({ token, config })
} catch (err) {
return stopDeployment(`Unknown error: ${err}\n${err.stack}`)
}
}
async function sync({ token, config: { currentTeam, user } }) {
const start = Date.now()
@ -248,19 +251,23 @@ async function sync({ token, config: { currentTeam, user } }) {
currentTeam
}).getCurrent()
const stopDeployment = msg => {
error(msg)
process.exit(1)
}
const isValidRepo = isRepoPath(rawPath)
try {
await fs.stat(path)
} catch (err) {
let repo
let isValidRepo = false
try {
isValidRepo = isRepoPath(rawPath)
} catch (err) {
if (err.code === 'INVALID_URL') {
stopDeployment(err)
} else {
throw err
}
}
if (isValidRepo && isValidRepo !== 'no-valid-url') {
if (isValidRepo) {
const gitParts = gitPathParts(rawPath)
Object.assign(gitRepo, gitParts)
@ -282,8 +289,6 @@ async function sync({ token, config: { currentTeam, user } }) {
// Set global variable for deleting tmp dir later
// once the deployment has finished
Object.assign(gitRepo, repo)
} else if (isValidRepo === 'no-valid-url') {
stopDeployment(`This URL is neither a valid repository from GitHub, nor from GitLab.`)
} else if (isValidRepo) {
const gitRef = gitRepo.ref ? `with "${chalk.bold(gitRepo.ref)}" ` : ''
stopDeployment(`There's no repository named "${chalk.bold(gitRepo.main)}" ${gitRef}on ${gitRepo.type}`)
@ -309,12 +314,10 @@ async function sync({ token, config: { currentTeam, user } }) {
}
}
let nowConfig
let deploymentType
let hasPackage
let hasDockerfile
let isStatic
// CLI deployment type explicit overrides
if (argv.docker) {
if (debug) {
console.log(`> [debug] Forcing \`deploymentType\` = \`docker\``)
@ -322,101 +325,89 @@ async function sync({ token, config: { currentTeam, user } }) {
deploymentType = 'docker'
} else if (argv.npm) {
deploymentType = 'npm'
} else if (argv.static) {
if (debug) {
console.log(`> [debug] Forcing static deployment`)
console.log(`> [debug] Forcing \`deploymentType\` = \`npm\``)
}
deploymentType = 'npm'
isStatic = true
} else {
try {
await fs.stat(resolve(path, 'package.json'))
} catch (err) {
hasPackage = true
} else if (argv.static) {
if (debug) {
console.log(`> [debug] Forcing \`deploymentType\` = \`static\``)
}
;[hasPackage, hasDockerfile] = await Promise.all([
await (async () => {
try {
await fs.stat(resolve(path, 'package.json'))
} catch (err) {
return false
}
return true
})(),
await (async () => {
try {
await fs.stat(resolve(path, 'Dockerfile'))
} catch (err) {
return false
deploymentType = 'static'
}
let meta
await retry(
async bail => {
try {
meta = await readMetaData(path, {
deploymentType,
deploymentName,
quiet: true
})
nowConfig = meta.nowConfig
if (!deploymentType) {
deploymentType = meta.type
if (debug) {
console.log(`> [debug] Detected \`deploymentType\` = \`${deploymentType}\``)
}
}
return true
})()
])
if (hasPackage && hasDockerfile) {
if (debug) {
console.log('[debug] multiple manifests found, disambiguating')
}
if (!deploymentName) {
deploymentName = meta.name
if (isTTY) {
try {
console.log(`> Two manifests found. Press [${chalk.bold('n')}] to deploy or re-run with --flag`)
deploymentType = await promptOptions([
[
'npm',
`${chalk.bold('package.json')}\t${chalk.gray(' --npm')} `
],
[
'docker',
`${chalk.bold('Dockerfile')}\t${chalk.gray('--docker')} `
]
])
} catch (err) {
error(err.message)
process.exit(1)
if (debug) {
console.log(`> [debug] Detected \`deploymentName\` = "${deploymentName}"`)
}
}
} else {
error(
'Ambiguous deployment (`package.json` and `Dockerfile` found). ' +
'Please supply `--npm` or `--docker` to disambiguate.'
)
}
} else if (hasPackage) {
if (debug) {
console.log(
'> [debug] `package.json` found, assuming `deploymentType` = `npm`'
)
}
} catch (err) {
if (err.code === 'MULTIPLE_MANIFESTS') {
if (debug) {
console.log('> [debug] Multiple manifests found, disambiguating')
}
deploymentType = 'npm'
} else if (hasDockerfile) {
if (debug) {
console.log(
'> [debug] `Dockerfile` found, assuming `deploymentType` = `docker`'
)
}
if (isTTY) {
console.log(`> Two manifests found. Press [${chalk.bold('n')}] to deploy or re-run with --flag`)
try {
deploymentType = await promptOptions([
[
'npm',
`${chalk.bold('package.json')}\t${chalk.gray(' --npm')} `
],
[
'docker',
`${chalk.bold('Dockerfile')}\t${chalk.gray('--docker')} `
]
])
} catch (err) {
console.error(err)
return bail()
}
if (debug) {
console.log(`> [debug] Selected \`deploymentType\` = "${deploymentType}"`)
}
// Invoke async-retry and try again with the explicit deployment type
throw err
}
}
deploymentType = 'docker'
} else {
if (debug) {
console.log(
'> [debug] No manifest files found, assuming static deployment'
)
return stopDeployment(err)
}
isStatic = true
},
{
retries: 1,
minTimeout: 0,
maxTimeout: 0,
onRetry: console.log
}
}
const { nowConfig } = await readMetaData(path, {
deploymentType,
deploymentName,
isStatic,
quiet: true
})
)
const now = new Now({ apiUrl, token, debug, currentTeam })
@ -529,18 +520,21 @@ async function sync({ token, config: { currentTeam, user } }) {
})
try {
await now.create(path, {
env,
deploymentType,
deploymentName,
followSymlinks,
forceNew,
forceSync,
forwardNpm: alwaysForwardNpm || forwardNpm,
quiet,
wantsPublic,
isStatic
})
await now.create(
path,
Object.assign(
{
env,
followSymlinks,
forceNew,
forceSync,
forwardNpm: alwaysForwardNpm || forwardNpm,
quiet,
wantsPublic
},
meta
)
)
} catch (err) {
if (debug) {
console.log(`> [debug] error: ${err}\n${err.stack}`)
@ -725,3 +719,5 @@ function printLogs(host, token, currentTeam, user) {
process.exit(0)
})
}
main()

10
lib/get-files.js

@ -77,11 +77,11 @@ const asAbsolute = function(path, parent) {
async function npm(
path,
pkg,
nowConfig = null,
pkg = {},
nowConfig = {},
{ limit = null, hasNowJson = false, debug = false } = {}
) {
const whitelist = (nowConfig && nowConfig.files) || pkg.files
const whitelist = nowConfig.files || pkg.files
// The package.json `files` whitelist still
// honors ignores: https://docs.npmjs.com/files/package.json#files
@ -172,10 +172,10 @@ async function npm(
async function docker(
path,
nowConfig = null,
nowConfig = {},
{ limit = null, hasNowJson = false, debug = false } = {}
) {
const whitelist = nowConfig && nowConfig.files
const whitelist = nowConfig.files
// Base search path
// the now.json `files` whitelist still

5
lib/git.js

@ -183,7 +183,10 @@ const isRepoPath = path => {
return true
}
return 'no-valid-url'
const err = new Error(`Host "${urlParts.host}" is unsupported.`)
err.code = 'INVALID_URL'
err.userError = true
throw err
}
return /[^\s\\]\/[^\s\\]/g.test(path)

86
lib/index.js

@ -20,12 +20,8 @@ const { npm: getNpmFiles, docker: getDockerFiles } = require('./get-files')
const ua = require('./ua')
const hash = require('./hash')
const Agent = require('./agent')
const readMetaData = require('./read-metadata')
const toHost = require('./to-host')
// Helpers
const { error } = require('./error')
// How many concurrent HTTP/2 stream uploads
const MAX_CONCURRENT = 10
@ -54,44 +50,44 @@ module.exports = class Now extends EventEmitter {
forceNew = false,
forceSync = false,
forwardNpm = false,
deploymentType = 'npm',
deploymentName,
isStatic = false
// From readMetaData
name,
description,
type = 'npm',
isStatic = false,
pkg = {},
nowConfig = {},
hasNowJson = false
}
) {
this._path = path
this._static = isStatic
let files
const meta = await readMetaData(path, {
deploymentType,
deploymentName,
quiet,
isStatic
})
const { pkg, name, description, nowConfig, hasNowJson } = meta
deploymentType = meta.deploymentType
let engines
if (this._debug) {
console.time('> [debug] Getting files')
}
const opts = { debug: this._debug, hasNowJson }
if (deploymentType === 'npm') {
if (type === 'npm') {
files = await getNpmFiles(path, pkg, nowConfig, opts)
// A `start` or `now-start` npm script, or a `server.js` file
// in the root directory of the deployment are required
if (!hasNpmStart(pkg) && !hasFile(path, files, 'server.js')) {
error(
const err = new Error(
'Missing `start` (or `now-start`) script in `package.json`. ' +
'See: https://docs.npmjs.com/cli/start.'
)
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1)
err.userError = true
throw err
}
engines = nowConfig.engines || pkg.engines
forwardNpm = forwardNpm || nowConfig.forwardNpm
} else {
files = await getDockerFiles(path, nowConfig, opts)
}
@ -100,40 +96,18 @@ module.exports = class Now extends EventEmitter {
console.timeEnd('> [debug] Getting files')
}
forwardNpm = forwardNpm || (nowConfig && nowConfig.forwardNpm)
// Read .npmrc
let npmrc = {}
// Read `registry.npmjs.org` authToken from .npmrc
let authToken
if (deploymentType === 'npm' && forwardNpm) {
try {
npmrc = await readFile(resolvePath(path, '.npmrc'), 'utf8')
npmrc = parseIni(npmrc)
authToken = npmrc['//registry.npmjs.org/:_authToken']
} catch (err) {
// Do nothing
}
if (!authToken) {
try {
npmrc = await readFile(resolvePath(homedir(), '.npmrc'), 'utf8')
npmrc = parseIni(npmrc)
authToken = npmrc['//registry.npmjs.org/:_authToken']
} catch (err) {
// Do nothing
}
}
if (type === 'npm' && forwardNpm) {
authToken =
(await readAuthToken(path)) || (await readAuthToken(homedir()))
}
if (this._debug) {
console.time('> [debug] Computing hashes')
}
const pkgDetails = {}
pkgDetails.name = name
Object.assign(pkgDetails, pkg)
const pkgDetails = Object.assign({ name }, pkg)
const hashes = await hash(files, isStatic, pkgDetails)
if (this._debug) {
@ -142,8 +116,6 @@ module.exports = class Now extends EventEmitter {
this._files = hashes
const engines = (nowConfig && nowConfig.engines) || pkg.engines
const deployment = await this.retry(async bail => {
if (this._debug) {
console.time('> [debug] /now/create')
@ -198,7 +170,7 @@ module.exports = class Now extends EventEmitter {
forceSync,
name,
description,
deploymentType,
deploymentType: type,
registryAuthToken: authToken,
files,
engines
@ -270,7 +242,7 @@ module.exports = class Now extends EventEmitter {
}
}
if (!quiet && deploymentType === 'npm' && deployment.nodeVersion) {
if (!quiet && type === 'npm' && deployment.nodeVersion) {
if (engines && engines.node) {
if (missingVersion) {
console.log(`> Using Node.js ${chalk.bold(deployment.nodeVersion)} (default)`)
@ -997,3 +969,13 @@ function hasFile(base, files, name) {
const relative = files.map(file => toRelative(file, base))
return relative.indexOf(name) !== -1
}
async function readAuthToken(path, name = '.npmrc') {
try {
const contents = await readFile(resolvePath(path, name), 'utf8')
const npmrc = parseIni(contents)
return npmrc['//registry.npmjs.org/:_authToken']
} catch (err) {
// Do nothing
}
}

237
lib/read-metadata.js

@ -3,119 +3,111 @@ const { basename, resolve: resolvePath } = require('path')
// Packages
const chalk = require('chalk')
const { readFile, exists } = require('fs-promise')
const { readFile } = require('fs-promise')
const { parse: parseDockerfile } = require('docker-file-parser')
// Helpers
const { error } = require('../lib/error')
const listPackage = {
// `package.json` used for "static" deployments
const staticPackage = Object.freeze({
scripts: {
start: `NODE_ENV='production' serve ./content`
},
dependencies: {
serve: '5.0.4'
}
}
})
module.exports = readMetaData
async function readMetaData(
path,
{
deploymentType = 'npm',
deploymentName,
quiet = false,
strict = true,
isStatic = false
}
{ deploymentType, deploymentName, quiet = false, strict = true }
) {
let pkg = {}
let nowConfig = null
let hasNowJson = false
let name
let description
let type = deploymentType
let name = deploymentName
try {
nowConfig = JSON.parse(await readFile(resolvePath(path, 'now.json')))
hasNowJson = true
} catch (err) {
// If the file doesn't exist then that's fine; any other error bubbles up
if (err.code !== 'ENOENT') {
const e = Error(`Failed to read JSON in "${path}/now.json"`)
e.userError = true
throw e
let isStatic = false
let pkg = await readJSON(path, 'package.json')
let nowConfig = await readJSON(path, 'now.json')
const dockerfile = await readDockerfile(path)
const hasNowJson = Boolean(nowConfig)
if (pkg && pkg.now) {
// If the project has both a `now.json` and `now` Object in the `package.json`
// file, then fail hard and let the user know that they need to pick one or the
// other
if (nowConfig) {
const err = new Error(
'You have a `now` configuration field inside `package.json` ' +
'but configuration is also present in `now.json`! ' +
"Please ensure there's a single source of configuration by removing one."
)
err.userError = true
throw err
} else {
nowConfig = pkg.now
}
}
if (hasNowJson) {
// User can specify the type of deployment explicitly in the `now.json` file
// when both a package.json and Dockerfile exist
if (nowConfig.type) {
deploymentType = nowConfig.type
} else if (
!await exists(resolvePath(path, 'package.json')) &&
!await exists(resolvePath(path, 'Dockerfile'))
) {
deploymentType = 'static'
if (!type) {
// `now.json` / `pkg.now` get default type preference
if (nowConfig) {
type = nowConfig.type
}
if (nowConfig.name) {
deploymentName = nowConfig.name
// Both `package.json` and `Dockerfile` exist! Prompt the user to pick one.
if (!type && pkg && dockerfile) {
const err = new Error(
'Ambiguous deployment (`package.json` and `Dockerfile` found). ' +
'Please supply `--npm` or `--docker` to disambiguate.'
)
err.userError = true
err.code = 'MULTIPLE_MANIFESTS'
throw err
}
}
if (deploymentType === 'static') {
isStatic = true
deploymentType = 'npm'
}
if (!type && pkg) {
type = 'npm'
}
if (deploymentType === 'npm') {
if (isStatic) {
pkg = listPackage
} else {
try {
pkg = JSON.parse(await readFile(resolvePath(path, 'package.json')))
} catch (err) {
error(`Failed to read JSON in "${path}/package.json"`)
if (!type && dockerfile) {
type = 'docker'
}
// eslint-disable-next-line unicorn/no-process-exit
process.exit(1)
}
if (!type) {
type = 'static'
}
}
if (!deploymentName) {
if (typeof pkg.name === 'string' && pkg.name !== '') {
name = pkg.name
} else {
name = basename(path)
// "static" deployments are (not-so) secretly just
// npm deployments under the hood
if (type === 'static') {
isStatic = true
type = 'npm'
pkg = Object.assign({ name: pkg && pkg.name }, staticPackage)
}
if (!quiet && !isStatic) {
console.log(`> No \`name\` in \`package.json\`, using ${chalk.bold(name)}`)
}
}
}
if (!name && nowConfig) {
name = nowConfig.name
}
description = pkg.description
} else if (deploymentType === 'docker') {
let docker
try {
const dockerfile = await readFile(resolvePath(path, 'Dockerfile'), 'utf8')
docker = parseDockerfile(dockerfile, { includeComments: true })
} catch (err) {
const e = Error(`Failed to parse "${path}/Dockerfile"`)
e.userError = true
throw e
if (type === 'npm') {
if (pkg) {
if (!name && pkg.name) {
name = String(pkg.name)
}
description = pkg.description
}
if (strict && docker.length <= 0) {
const e = Error('No commands found in `Dockerfile`')
e.userError = true
throw e
} else if (type === 'docker') {
if (strict && dockerfile.length <= 0) {
const err = new Error('No commands found in `Dockerfile`')
err.userError = true
throw err
}
const labels = {}
docker.filter(cmd => cmd.name === 'LABEL').forEach(({ args }) => {
dockerfile.filter(cmd => cmd.name === 'LABEL').forEach(({ args }) => {
for (const key in args) {
if (!{}.hasOwnProperty.call(args, key)) {
continue
@ -125,61 +117,74 @@ async function readMetaData(
try {
labels[key] = args[key]
} catch (err) {
const e = Error(`Error parsing value for LABEL ${key} in \`Dockerfile\``)
const e = new Error(
`Error parsing value for LABEL ${key} in \`Dockerfile\``
)
e.userError = true
throw e
}
}
})
if (!deploymentName) {
if (labels.name) {
name = labels.name
} else {
name = basename(path)
if (!quiet) {
if (hasNowJson) {
console.log(`> No \`name\` LABEL in \`Dockerfile\` or \`name\` field in \`now.json\`, using ${chalk.bold(name)}`)
} else {
console.log(`> No \`name\` LABEL in \`Dockerfile\`, using ${chalk.bold(name)}`)
}
}
}
if (!name) {
name = labels.name
}
description = labels.description
} else {
throw new TypeError(`Unsupported "deploymentType": ${deploymentType}`)
throw new TypeError(`Unsupported "deploymentType": ${type}`)
}
if (deploymentName) {
name = deploymentName
}
// No name in `package.json` / `now.json`, or "name" label in Dockerfile.
// Default to the basename of the root dir
if (!name) {
name = basename(path)
if (pkg.now) {
// If the project has both a `now.json` and `now` Object in the `package.json`
// file, then fail hard and let the user know that they need to pick one or the
// other
if (hasNowJson) {
const e = new Error(
'You have a `now` configuration field' +
'inside `package.json`, but configuration is also present' +
"in `now.json`! Please ensure there's a single source of configuration by removing one"
)
e.userError = true
throw e
} else {
nowConfig = pkg.now
if (!quiet && !isStatic) {
if (type === 'docker') {
console.log(`> No \`name\` LABEL in \`Dockerfile\`, using ${chalk.bold(name)}`)
} else {
console.log(`> No \`name\` in \`package.json\`, using ${chalk.bold(name)}`)
}
}
}
return {
name,
description,
deploymentType,
type,
isStatic,
pkg,
nowConfig,
hasNowJson
hasNowJson,
// XXX: legacy
deploymentType: type
}
}
async function readJSON(path, name) {
try {
const contents = await readFile(resolvePath(path, name), 'utf8')
return JSON.parse(contents)
} catch (err) {
// If the file doesn't exist then that's fine; any other error bubbles up
if (err.code !== 'ENOENT') {
err.userError = true
throw err
}
}
}
async function readDockerfile(path, name = 'Dockerfile') {
try {
const contents = await readFile(resolvePath(path, name), 'utf8')
return parseDockerfile(contents, { includeComments: true })
} catch (err) {
// If the file doesn't exist then that's fine; any other error bubbles up
if (err.code !== 'ENOENT') {
err.userError = true
throw err
}
}
}

1
test/_fixtures/multiple-manifests-throws/Dockerfile

@ -0,0 +1 @@
CMD echo 'world'

3
test/_fixtures/multiple-manifests-throws/package.json

@ -0,0 +1,3 @@
{
"name": "simple"
}

1
test/_fixtures/now-json-docker/now.json

@ -1,5 +1,4 @@
{
"type": "docker",
"files": [
"b.js"
]

1
test/_fixtures/type-in-package-now-with-dockerfile/Dockerfile

@ -0,0 +1 @@
CMD echo 'world'

5
test/_fixtures/type-in-package-now-with-dockerfile/package.json

@ -0,0 +1,5 @@
{
"now": {
"type": "npm"
}
}

26
test/index.js

@ -212,3 +212,29 @@ test('throws when both `now.json` and `package.json:now` exist', async t => {
/please ensure there's a single source of configuration/i.test(e.message)
)
})
test('throws when `package.json` and `Dockerfile` exist', async t => {
let e
try {
await readMetadata(fixture('multiple-manifests-throws'), {
quiet: true,
strict: false
})
} catch (err) {
e = err
}
t.is(e.userError, true)
t.is(e.code, 'MULTIPLE_MANIFESTS')
t.pass(/ambiguous deployment/i.test(e.message))
})
test('support `package.json:now.type` to bypass multiple manifests error', async t => {
const f = fixture('type-in-package-now-with-dockerfile')
const { type, nowConfig, hasNowJson } = await readMetadata(f, {
quiet: true,
strict: false
})
t.is(type, 'npm')
t.is(nowConfig.type, 'npm')
t.is(hasNowJson, false)
})

Loading…
Cancel
Save