Skip to content

Commit

Permalink
chore(gatsby) Migrate joi schema to Typescript (#22738)
Browse files Browse the repository at this point in the history
* Migrate joi schema to Typescript

* Add joi schema for gatsby-cli's structured-errors as well

* Fixes unit-test

* Use types from redux/types - they seem to be newer than types in gatsby/index.d.ts

* consolidate structured errors types into types file to be imported by others

* force rebuild

Co-authored-by: madalynrose <madalyn@gatsbyjs.com>
  • Loading branch information
arthurjdam and madalynrose committed Apr 3, 2020
1 parent 9bc2026 commit 5b35acc
Show file tree
Hide file tree
Showing 12 changed files with 122 additions and 107 deletions.
@@ -1,12 +1,12 @@
import schema from "../error-schema"
import { errorSchema } from "../error-schema"

test(`throws invalid on an invalid error`, () => {
expect(schema.validate({ lol: `true` })).rejects.toBeDefined()
expect(errorSchema.validate({ lol: `true` })).rejects.toBeDefined()
})

test(`does not throw on a valid schema`, () => {
expect(
schema.validate({
errorSchema.validate({
context: {},
})
).resolves.toEqual(expect.any(Object))
Expand Down
44 changes: 4 additions & 40 deletions packages/gatsby-cli/src/structured-errors/construct-error.ts
@@ -1,44 +1,8 @@
import Joi from "@hapi/joi"
import stackTrace from "stack-trace"
import errorSchema from "./error-schema"
import { errorMap, defaultError, IErrorMapEntry, ErrorId } from "./error-map"
import { errorSchema } from "./error-schema"
import { errorMap, defaultError, IErrorMapEntry } from "./error-map"
import { sanitizeStructuredStackTrace } from "../reporter/errors"

interface IConstructError {
details: {
id?: ErrorId
context?: Record<string, string>
error?: Error
[key: string]: unknown
}
}

interface ILocationPosition {
line: number
column: number
}

interface IStructuredError {
code?: string
text: string
stack: {
fileName: string
functionName?: string
lineNumber?: number
columnNumber?: number
}[]
filePath?: string
location?: {
start: ILocationPosition
end?: ILocationPosition
}
error?: unknown
group?: string
level: IErrorMapEntry["level"]
type?: IErrorMapEntry["type"]
docsUrl?: string
}

import { IConstructError, IStructuredError } from "./types"
// Merge partial error details with information from the errorMap
// Validate the constructed object against an error schema
const constructError = ({
Expand All @@ -63,7 +27,7 @@ const constructError = ({
}

// validate
const { error } = Joi.validate(structuredError, errorSchema)
const { error } = errorSchema.validate(structuredError)
if (error !== null) {
console.log(`Failed to validate error`, error)
process.exit(1)
Expand Down
22 changes: 1 addition & 21 deletions packages/gatsby-cli/src/structured-errors/error-map.ts
@@ -1,25 +1,5 @@
import { stripIndent, stripIndents } from "common-tags"

interface IOptionalGraphQLInfoContext {
codeFrame?: string
filePath?: string
urlPath?: string
plugin?: string
}

enum Level {
ERROR = `ERROR`,
WARNING = `WARNING`,
INFO = `INFO`,
DEBUG = `DEBUG`,
}

enum Type {
GRAPHQL = `GRAPHQL`,
CONFIG = `CONFIG`,
WEBPACK = `WEBPACK`,
PLUGIN = `PLUGIN`,
}
import { IOptionalGraphQLInfoContext, Level, Type } from "./types"

const optionalGraphQLInfo = (context: IOptionalGraphQLInfoContext): string =>
`${context.codeFrame ? `\n\n${context.codeFrame}` : ``}${
Expand Down
65 changes: 33 additions & 32 deletions packages/gatsby-cli/src/structured-errors/error-schema.ts
@@ -1,38 +1,39 @@
import Joi from "@hapi/joi"
import { ILocationPosition, IStructuredError } from "./types"

const Position = Joi.object().keys({
export const Position: Joi.ObjectSchema<ILocationPosition> = Joi.object().keys({
line: Joi.number(),
column: Joi.number(),
})

const errorSchema = Joi.object().keys({
code: Joi.string(),
text: Joi.string(),
stack: Joi.array()
.items(
Joi.object().keys({
fileName: Joi.string(),
functionName: Joi.string().allow(null),
lineNumber: Joi.number().allow(null),
columnNumber: Joi.number().allow(null),
})
)
.allow(null),
level: Joi.string().valid([`ERROR`, `WARNING`, `INFO`, `DEBUG`]),
type: Joi.string().valid([`GRAPHQL`, `CONFIG`, `WEBPACK`, `PLUGIN`]),
filePath: Joi.string(),
location: Joi.object({
start: Position.required(),
end: Position,
}),
docsUrl: Joi.string().uri({
allowRelative: false,
relativeOnly: false,
}),
error: Joi.object({}).unknown(),
context: Joi.object({}).unknown(),
group: Joi.string(),
panicOnBuild: Joi.boolean(),
})

export default errorSchema
export const errorSchema: Joi.ObjectSchema<IStructuredError> = Joi.object().keys(
{
code: Joi.string(),
text: Joi.string(),
stack: Joi.array()
.items(
Joi.object().keys({
fileName: Joi.string(),
functionName: Joi.string().allow(null),
lineNumber: Joi.number().allow(null),
columnNumber: Joi.number().allow(null),
})
)
.allow(null),
level: Joi.string().valid([`ERROR`, `WARNING`, `INFO`, `DEBUG`]),
type: Joi.string().valid([`GRAPHQL`, `CONFIG`, `WEBPACK`, `PLUGIN`]),
filePath: Joi.string(),
location: Joi.object({
start: Position.required(),
end: Position,
}),
docsUrl: Joi.string().uri({
allowRelative: false,
relativeOnly: false,
}),
error: Joi.object({}).unknown(),
context: Joi.object({}).unknown(),
group: Joi.string(),
panicOnBuild: Joi.boolean(),
}
)
57 changes: 57 additions & 0 deletions packages/gatsby-cli/src/structured-errors/types.ts
@@ -0,0 +1,57 @@
import { IErrorMapEntry, ErrorId } from "./error-map"

export interface IConstructError {
details: {
id?: ErrorId
context?: Record<string, string>
error?: Error
[key: string]: unknown
}
}

export interface ILocationPosition {
line: number
column: number
}

export interface IStructuredError {
code?: string
text: string
stack: {
fileName: string
functionName?: string
lineNumber?: number
columnNumber?: number
}[]
filePath?: string
location?: {
start: ILocationPosition
end?: ILocationPosition
}
error?: unknown
group?: string
level: IErrorMapEntry["level"]
type?: IErrorMapEntry["type"]
docsUrl?: string
}

export interface IOptionalGraphQLInfoContext {
codeFrame?: string
filePath?: string
urlPath?: string
plugin?: string
}

export enum Level {
ERROR = `ERROR`,
WARNING = `WARNING`,
INFO = `INFO`,
DEBUG = `DEBUG`,
}

export enum Type {
GRAPHQL = `GRAPHQL`,
CONFIG = `CONFIG`,
WEBPACK = `WEBPACK`,
PLUGIN = `PLUGIN`,
}
2 changes: 2 additions & 0 deletions packages/gatsby/index.d.ts
Expand Up @@ -176,6 +176,8 @@ export interface GatsbyConfig {
>
/** It’s common for sites to be hosted somewhere other than the root of their domain. Say we have a Gatsby site at `example.com/blog/`. In this case, we would need a prefix (`/blog`) added to all paths on the site. */
pathPrefix?: string
/** In some circumstances you may want to deploy assets (non-HTML resources such as JavaScript, CSS, etc.) to a separate domain. `assetPrefix` allows you to use Gatsby with assets hosted from a separate domain */
assetPrefix?: string
/** Gatsby uses the ES6 Promise API. Because some browsers don't support this, Gatsby includes a Promise polyfill by default. If you'd like to provide your own Promise polyfill, you can set `polyfill` to false.*/
polyfill?: boolean
mapping?: Record<string, string>
Expand Down
1 change: 1 addition & 0 deletions packages/gatsby/package.json
Expand Up @@ -150,6 +150,7 @@
"devDependencies": {
"@babel/cli": "^7.8.4",
"@babel/runtime": "^7.8.7",
"@types/hapi__joi": "^16.0.12",
"@types/socket.io": "^2.1.4",
"babel-preset-gatsby-package": "^0.3.1",
"cross-env": "^5.2.1",
Expand Down
@@ -1,4 +1,4 @@
const { gatsbyConfigSchema, nodeSchema } = require(`../joi`)
import { gatsbyConfigSchema, nodeSchema } from "../joi"

describe(`gatsby config`, () => {
it(`returns empty pathPrefix when not set`, async () => {
Expand Down Expand Up @@ -133,7 +133,8 @@ describe(`node schema`, () => {

const { error } = nodeSchema.validate(node)
expect(error).toBeTruthy()
expect(error.message).toMatchSnapshot()
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(error!.message).toMatchSnapshot()
})

it(`doesn't allow unknown internal fields`, async () => {
Expand All @@ -152,6 +153,7 @@ describe(`node schema`, () => {

const { error } = nodeSchema.validate(node)
expect(error).toBeTruthy()
expect(error.message).toMatchSnapshot()
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(error!.message).toMatchSnapshot()
})
})
@@ -1,13 +1,16 @@
const Joi = require(`@hapi/joi`)
import Joi from "@hapi/joi"
import { IGatsbyConfig, IGatsbyPage, IGatsbyNode } from "../redux/types"

const stripTrailingSlash = (chain: Joi.StringSchema): Joi.StringSchema =>
chain.replace(/(\w)\/+$/, `$1`)

const stripTrailingSlash = chain => chain.replace(/(\w)\/+$/, `$1`)
// only add leading slash on relative urls
const addLeadingSlash = chain =>
const addLeadingSlash = (chain: Joi.StringSchema): Joi.StringSchema =>
chain.when(Joi.string().uri({ relativeOnly: true }), {
then: chain.replace(/^([^/])/, `/$1`),
})

export const gatsbyConfigSchema = Joi.object()
export const gatsbyConfigSchema: Joi.ObjectSchema<IGatsbyConfig> = Joi.object()
.keys({
__experimentalThemes: Joi.array(),
polyfill: Joi.boolean().default(true),
Expand Down Expand Up @@ -73,7 +76,7 @@ export const gatsbyConfigSchema = Joi.object()
}
)

export const pageSchema = Joi.object()
export const pageSchema: Joi.ObjectSchema<IGatsbyPage> = Joi.object()
.keys({
path: Joi.string().required(),
matchPath: Joi.string(),
Expand All @@ -85,7 +88,7 @@ export const pageSchema = Joi.object()
})
.unknown()

export const nodeSchema = Joi.object()
export const nodeSchema: Joi.ObjectSchema<IGatsbyNode> = Joi.object()
.keys({
id: Joi.string().required(),
children: Joi.array().items(Joi.string(), Joi.object().forbidden()),
Expand Down
4 changes: 2 additions & 2 deletions packages/gatsby/src/redux/actions/public.js
Expand Up @@ -14,7 +14,7 @@ const { hasNodeChanged, getNode } = require(`../../db/nodes`)
const sanitizeNode = require(`../../db/sanitize-node`)
const { store } = require(`..`)
const fileExistsSync = require(`fs-exists-cached`).sync
const joiSchemas = require(`../../joi-schemas/joi`)
import { nodeSchema } from "../../joi-schemas/joi"
const { generateComponentChunkName } = require(`../../utils/js-chunk-names`)
const {
getCommonDir,
Expand Down Expand Up @@ -744,7 +744,7 @@ const createNode = (

trackCli(`CREATE_NODE`, trackParams, { debounce: true })

const result = Joi.validate(node, joiSchemas.nodeSchema)
const result = Joi.validate(node, nodeSchema)
if (result.error) {
if (!hasErroredBecauseOfNodeValidation.has(result.error.message)) {
const errorObj = {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Expand Up @@ -3927,6 +3927,11 @@
"@types/tough-cookie" "*"
form-data "^2.5.0"

"@types/hapi__joi@^16.0.12":
version "16.0.12"
resolved "https://registry.yarnpkg.com/@types/hapi__joi/-/hapi__joi-16.0.12.tgz#fb9113f17cf5764d6b3586ae9817d1606cc7c90c"
integrity sha512-xJYifuz59jXdWY5JMS15uvA3ycS3nQYOGqoIIE0+fwQ0qI3/4CxBc6RHsOTp6wk9M0NWEdpcTl02lOQOKMifbQ==

"@types/history@*":
version "4.7.0"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.0.tgz#2fac51050c68f7d6f96c5aafc631132522f4aa3f"
Expand Down

0 comments on commit 5b35acc

Please sign in to comment.