Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 20 additions & 87 deletions package-lock.json

Large diffs are not rendered by default.

27 changes: 18 additions & 9 deletions packages/config/src/error.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
// We distinguish between errors thrown intentionally and uncaught exceptions
// (such as bugs) with a `customErrorInfo.type` property.
export const throwUserError = function (messageOrError: string | Error, error?: Error) {
const errorA = getError(messageOrError, error)

const CUSTOM_ERROR_KEY = 'customErrorInfo'
const USER_ERROR_TYPE = 'resolveConfig'

interface CustomErrorInfo {
type: string
}

interface CustomError extends Error {
[CUSTOM_ERROR_KEY]?: CustomErrorInfo
}

export const throwUserError = function (messageOrError: string | Error, error?: Error): never {
const errorA: CustomError = getError(messageOrError, error)
errorA[CUSTOM_ERROR_KEY] = { type: USER_ERROR_TYPE }
throw errorA
}

// Can pass either `message`, `error` or `message, error`
const getError = function (messageOrError: string | Error, error?: Error) {
const getError = function (messageOrError: string | Error, error?: Error): Error {
if (messageOrError instanceof Error) {
return messageOrError
}
Expand All @@ -20,17 +32,14 @@ const getError = function (messageOrError: string | Error, error?: Error) {
return error
}

export const isUserError = function (error) {
export const isUserError = function (error: any): error is CustomError {
return (
canHaveErrorInfo(error) && error[CUSTOM_ERROR_KEY] !== undefined && error[CUSTOM_ERROR_KEY].type === USER_ERROR_TYPE
)
}

// Exceptions that are not objects (including `Error` instances) cannot have an
// `CUSTOM_ERROR_KEY` property
const canHaveErrorInfo = function (error) {
return error != null
const canHaveErrorInfo = function (error: any): error is object {
return error != null && typeof error === 'object'
}

const CUSTOM_ERROR_KEY = 'customErrorInfo'
const USER_ERROR_TYPE = 'resolveConfig'
38 changes: 0 additions & 38 deletions packages/config/src/origin.js

This file was deleted.

90 changes: 90 additions & 0 deletions packages/config/src/origin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { isTruthy } from './utils/remove_falsy.js'

export type Origin = 'ui' | 'config' | 'default' | 'inline'

interface Plugin {
package: string
origin?: Origin
[key: string]: any
}

interface Config {
build?: {
command?: string
commandOrigin?: Origin
publish?: string
publishOrigin?: Origin
[key: string]: any
}
plugins?: Plugin[]
headers?: any
headersOrigin?: Origin
redirects?: any
redirectsOrigin?: Origin
[key: string]: any
}

// `build.commandOrigin`, `build.publishOrigin` and `plugins[*].origin` constants
export const UI_ORIGIN: Origin = 'ui'
export const CONFIG_ORIGIN: Origin = 'config'
export const DEFAULT_ORIGIN: Origin = 'default'
export const INLINE_ORIGIN: Origin = 'inline'

// Add `build.commandOrigin`, `build.publishOrigin` and `plugins[*].origin`.
// This shows whether those properties came from the `ui` or from the `config`.
export const addOrigins = function (config: Config, origin: Origin): Config {
const configA = addBuildCommandOrigin({ config, origin })
const configB = addBuildPublishOrigin({ config: configA, origin })
const configC = addConfigPluginOrigin({ config: configB, origin })
const configD = addHeadersOrigin({ config: configC, origin })
const configE = addRedirectsOrigin({ config: configD, origin })
return configE
}

const addBuildCommandOrigin = function ({
config,
config: { build = {} },
origin,
}: {
config: Config
origin: Origin
}): Config {
return isTruthy(build.command) ? { ...config, build: { ...build, commandOrigin: origin } } : config
}

const addBuildPublishOrigin = function ({
config,
config: { build = {} },
origin,
}: {
config: Config
origin: Origin
}): Config {
return isTruthy(build.publish) ? { ...config, build: { ...build, publishOrigin: origin } } : config
}

const addConfigPluginOrigin = function ({
config,
config: { plugins },
origin,
}: {
config: Config
origin: Origin
}): Config {
return Array.isArray(plugins) ? { ...config, plugins: plugins.map((plugin) => ({ origin, ...plugin })) } : config
}

const addHeadersOrigin = function ({ config, config: { headers }, origin }: { config: Config; origin: Origin }): Config {
return isTruthy(headers) ? { ...config, headersOrigin: origin } : config
}

const addRedirectsOrigin = function ({
config,
config: { redirects },
origin,
}: {
config: Config
origin: Origin
}): Config {
return isTruthy(redirects) ? { ...config, redirectsOrigin: origin } : config
}
12 changes: 6 additions & 6 deletions packages/config/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,38 +25,38 @@ export const parseConfig = async function (configPath?: string) {
* Same but `configPath` is required and `configPath` might point to a
* non-existing file.
*/
export const parseOptionalConfig = async function (configPath) {
export const parseOptionalConfig = async function (configPath: string) {
if (!existsSync(configPath)) {
return {}
}

return await readConfigPath(configPath)
}

const readConfigPath = async function (configPath) {
const readConfigPath = async function (configPath: string) {
const configString = await readConfig(configPath)

validateTomlBlackslashes(configString)

try {
return parseToml(configString)
} catch (error) {
throwUserError('Could not parse configuration file', error)
throwUserError('Could not parse configuration file', error as Error)
}
}

/**
* Reach the configuration file's raw content
*/
const readConfig = async function (configPath) {
const readConfig = async function (configPath: string): Promise<string> {
try {
return await fs.readFile(configPath, 'utf8')
} catch (error) {
throwUserError('Could not read configuration file', error)
return throwUserError('Could not read configuration file', error as Error)
}
}

const validateTomlBlackslashes = function (configString) {
const validateTomlBlackslashes = function (configString: string) {
const result = INVALID_TOML_BLACKSLASH.exec(configString)
if (result === null) {
return
Expand Down
10 changes: 0 additions & 10 deletions packages/config/src/utils/group.js

This file was deleted.

12 changes: 12 additions & 0 deletions packages/config/src/utils/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Group objects according to a key attribute.
* The key must exist in each object and be a string.
*/
export const groupBy = function <T, K extends keyof T>(objects: T[], keyName: K): T[][] {
const keys = [...new Set(objects.map((object) => object[keyName]))]
return keys.map((key) => groupObjects(objects, keyName, key))
}

const groupObjects = function <T, K extends keyof T>(objects: T[], keyName: K, key: T[K]): T[] {
return objects.filter((object) => object[keyName] === key)
}
4 changes: 2 additions & 2 deletions packages/config/src/utils/remove_falsy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { includeKeys } from 'filter-obj'
/**
* Remove falsy values from object
*/
export const removeFalsy = function (obj) {
return includeKeys(obj, (_key, value) => isTruthy(value))
export const removeFalsy = <T extends object>(obj: T): Partial<T> => {
return includeKeys(obj, (_key, value) => isTruthy(value)) as Partial<T>
}

type NoUndefinedField<T> = { [P in keyof T]: Exclude<T[P], null | undefined> }
Expand Down
32 changes: 0 additions & 32 deletions packages/config/src/utils/set.js

This file was deleted.

38 changes: 38 additions & 0 deletions packages/config/src/utils/set.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import isPlainObj from 'is-plain-obj'

type Key = string | number

/**
* Set a property deeply using an array of `keys` which can be either strings
* (object properties) or integers (array indices).
* Adds default values when intermediary properties are undefined or have the
* wrong type. Also extends arrays when they are too small for a given index.
* Does not mutate.
*/
export const setProp = function (parent: unknown, keys: Key[], value: unknown): any {
if (keys.length === 0) {
return value
}

const [firstKey, ...restKeys] = keys

if (typeof firstKey === 'number') {
return setArrayProp(parent, firstKey, restKeys, value)
}

return setObjectProp(parent, firstKey as string, restKeys, value)
}

const setArrayProp = function (parent: unknown, index: number, keys: Key[], value: unknown): any[] {
const arrayParent = Array.isArray(parent) ? parent : []
const missingItems = index - arrayParent.length + 1
const normalizedParent = missingItems > 0 ? [...arrayParent, ...new Array(missingItems)] : arrayParent
const newValue = setProp(normalizedParent[index], keys, value)
return [...normalizedParent.slice(0, index), newValue, ...normalizedParent.slice(index + 1)]
}

const setObjectProp = function (parent: unknown, key: string, keys: Key[], value: unknown): object {
const objectParent = isPlainObj(parent) ? (parent as Record<string, unknown>) : {}
const newValue = setProp(objectParent[key], keys, value)
return { ...objectParent, [key]: newValue }
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { parse as loadToml } from '@iarna/toml'
import tomlify from 'tomlify-j0.4'

// Parse from TOML to JavaScript
export const parseToml = function (configString) {
/**
* Parse from TOML to JavaScript
*/
export const parseToml = function (configString: string): any {
const config = loadToml(configString)
// `toml.parse()` returns an object with `null` prototype deeply, which can
// sometimes create problems with some utilities. We convert it.
Expand All @@ -11,13 +13,17 @@ export const parseToml = function (configString) {
return JSON.parse(JSON.stringify(config))
}

// Serialize JavaScript object to TOML
export const serializeToml = function (object) {
/**
* Serialize JavaScript object to TOML
*/
export const serializeToml = function (object: any): string {
return tomlify.toToml(object, { space: 2, replace: replaceTomlValue })
}

// `tomlify-j0.4` serializes integers as floats, e.g. `200.0`.
// This is a problem with `redirects[*].status`.
const replaceTomlValue = function (key, value) {
/**
* `tomlify-j0.4` serializes integers as floats, e.g. `200.0`.
* This is a problem with `redirects[*].status`.
*/
const replaceTomlValue = function (key: string, value: any): string | boolean {
return Number.isInteger(value) ? String(value) : false
}
Loading