diff --git a/.circleci/config.yml b/.circleci/config.yml index c6adc9e3c..aec6c6fb4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,7 +14,7 @@ references: - image: cimg/node:<< parameters.node-version >> parameters: node-version: - default: '20.10' + default: '20.12' type: string workspace_root: &workspace_root ~/project @@ -184,21 +184,21 @@ workflows: name: build-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] - test: requires: - build-v<< matrix.node-version >> name: test-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] - lint: requires: - build-v<< matrix.node-version >> name: lint-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] release-please: when: @@ -228,21 +228,21 @@ workflows: name: build-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] - test: requires: - build-v<< matrix.node-version >> name: test-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] - lint: requires: - build-v<< matrix.node-version >> name: lint-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] build-test-publish: when: @@ -257,7 +257,7 @@ workflows: name: build-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] - test: filters: <<: *filters_release_build @@ -266,7 +266,7 @@ workflows: name: test-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] - lint: filters: <<: *filters_release_build @@ -275,14 +275,14 @@ workflows: name: lint-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] - publish: context: npm-publish-token filters: <<: *filters_release_build requires: - - lint-v20.10 - - test-v20.10 + - lint-v20.12 + - test-v20.12 build-test-prepublish: when: @@ -297,7 +297,7 @@ workflows: name: build-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] - test: filters: <<: *filters_prerelease_build @@ -306,7 +306,7 @@ workflows: name: test-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] - lint: filters: <<: *filters_prerelease_build @@ -315,14 +315,14 @@ workflows: name: lint-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] - prepublish: context: npm-publish-token filters: <<: *filters_prerelease_build requires: - - lint-v20.10 - - test-v20.10 + - lint-v20.12 + - test-v20.12 nightly: when: @@ -339,7 +339,7 @@ workflows: name: build-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] - test: requires: - build-v<< matrix.node-version >> @@ -347,7 +347,7 @@ workflows: name: test-v<< matrix.node-version >> matrix: parameters: - node-version: [ '16.14', '18.16', '20.10' ] + node-version: [ '18.20', '20.12' ] # Prior to producing a development orb (which requires credentials) basic validation, linting, and even unit testing can be performed. # This workflow will run on every commit diff --git a/.eslintrc.js b/.eslintrc.js index 8b3f39c2a..4911b0bf5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,9 +11,13 @@ module.exports = { rules: { // We use winston's logging instead 'no-console': 'error', + // conflicts with @typescript-eslint/no-unused-vars + 'no-unused-vars': 'off', // Necessary to allow us to define arguments in a method that only subclasses use // https://github.com/typescript-eslint/typescript-eslint/issues/586#issuecomment-510099609 - '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }] + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + // Prettier sometimes like inserting semis + '@typescript-eslint/no-extra-semi': 'off' }, overrides: [ { diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e8cdeea2e..9c108564e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -4,7 +4,7 @@ "lib/error": "3.2.0", "lib/logger": "3.4.0", "lib/options": "3.2.0", - "lib/package-json-hook": "4.2.0", + "plugins/package-json-hook": "4.2.0", "lib/state": "3.3.0", "lib/types": "3.6.0", "lib/vault": "3.2.0", diff --git a/contributing.md b/contributing.md new file mode 100644 index 000000000..6548b011e --- /dev/null +++ b/contributing.md @@ -0,0 +1,15 @@ +# Contributing + +Tool Kit is organised as a monorepo with all the different plugins and libraries stored in a single repository. This allows us to quickly investigate and make changes across the whole codebase, as well as making installation easier by sharing dependencies. See the [developer documentation](./docs/developing-tool-kit.md) for a full explanation of the internal architecture of Tool Kit. + +Release versions are not kept in sync between the packages, as we do not want to have to a major version bump for every package whenever we release a breaking change for a single package. + +We use [release-please](https://github.com/googleapis/release-please) to manage releases and versioning. Every time we make a merge to main, release-please checks which packages have been changed, and creates a PR to make new releases for them. It uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard to determine whether updates require a patch, minor, or major version bump, and we use [commitlint](https://commitlint.js.org) to enforce the standard in all of our commits. + +This means you should make an effort to think carefully about whether the changes you're making are a new feature or bug fix, and whether they contain any breaking changes. This might seem burdensome at first but it's good practice to make sure you can predict whether other teams' builds are going to break because of your code changes! If your commit will only affect a single package then please also include the name of the package (without the `@dotcom-tool-kit` namespace) in the scope of your commit message, as this makes it easier to see where changes are being made just by a quick glance at the git log. For example, a commit message for a new feature for the `circleci` plugin might look like: + +``` +feat(circleci): add support for nightly workflows +``` + +Note that new plugins should be created with a version number of `0.1.0`. This indicates that the package is still in the early stages of development and could be subject to many breaking changes before it's stabilised. Committing breaking changes whilst your package is `<1.0.0` are treated as minor bumps (`0.2.0`) and both new features and bug fixes as patch bumps (`0.1.1`.) When you're ready, you can release a 1.0 of your plugin by including `Release-As: 1.0.0` in the body of the release commit. diff --git a/core/cli/bin/run b/core/cli/bin/run index 22555b593..cea39aa18 100755 --- a/core/cli/bin/run +++ b/core/cli/bin/run @@ -1,22 +1,32 @@ #!/usr/bin/env node +/* eslint-disable @typescript-eslint/no-var-requires -- + * this is raw CJS so the lint doesn't apply + */ + const argv = require('minimist')(process.argv.slice(2), { boolean: ['help', 'install', 'listPlugins'], '--': true }) -const { runTasks, showHelp, installHooks, listPlugins } = require('../lib') const { rootLogger, styles } = require('@dotcom-tool-kit/logger') async function main() { try { if (argv.install) { + const installHooks = require('../lib/install').default await installHooks(rootLogger) } else if (argv.listPlugins) { + const { listPlugins } = require('../lib') await listPlugins(rootLogger) + } else if (argv.printConfig) { + const { printConfig } = require('../lib') + await printConfig(rootLogger) } else if (argv.help || argv._.length === 0) { + const showHelp = require('../lib/help').default await showHelp(rootLogger, argv._) } else { + const { runCommands } = require('../lib') if (argv['--'].length > 0) { // The `--` in a command such as `dotcom-tool-kit test:staged --` // delineates between hooks and file patterns. For example, when the @@ -25,9 +35,9 @@ async function main() { // the command becomes something like `dotcom-tool-kit test:staged -- // index.js`. When this command is executed it runs the configured task // where the file path arguments would then be extracted. - await runTasks(rootLogger, argv._, argv['--']) + await runCommands(rootLogger, argv._, argv['--']) } else { - await runTasks(rootLogger, argv._) + await runCommands(rootLogger, argv._) } } } catch (error) { diff --git a/core/cli/jest.config.js b/core/cli/jest.config.js index e549b3a36..9fbb18e03 100644 --- a/core/cli/jest.config.js +++ b/core/cli/jest.config.js @@ -1,6 +1,15 @@ const base = require('../../jest.config.base') +const path = require('path') module.exports = { - ...base, - globals: { 'ts-jest': { tsconfig: { resolveJsonModule: true } } } + ...base.config, + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + ...base.tsJestConfig, + tsconfig: path.resolve(__dirname, './tsconfig.test.json') + } + ] + } } diff --git a/core/cli/package.json b/core/cli/package.json index 27e29b919..7e4e48f54 100644 --- a/core/cli/package.json +++ b/core/cli/package.json @@ -1,6 +1,6 @@ { "name": "dotcom-tool-kit", - "version": "3.4.5", + "version": "4.0.0-beta.5", "description": "modern, maintainable, modular developer tooling for FT.com projects", "author": "FT.com Platforms Team ", "license": "MIT", @@ -20,41 +20,34 @@ "test": "cd ../../ ; npx jest --silent --projects core/cli" }, "devDependencies": { - "@dotcom-tool-kit/babel": "^3.2.0", - "@dotcom-tool-kit/backend-heroku-app": "^3.1.4", - "@dotcom-tool-kit/circleci": "^6.0.1", - "@dotcom-tool-kit/circleci-deploy": "^3.4.3", - "@dotcom-tool-kit/eslint": "^3.2.0", - "@dotcom-tool-kit/frontend-app": "^3.2.4", - "@dotcom-tool-kit/heroku": "^3.4.1", - "@dotcom-tool-kit/mocha": "^3.2.0", - "@dotcom-tool-kit/n-test": "^3.3.1", - "@dotcom-tool-kit/npm": "^3.3.1", - "@dotcom-tool-kit/webpack": "^3.2.0", "@jest/globals": "^27.4.6", "@types/lodash": "^4.14.185", "@types/node": "^16.18.23", "chai": "^4.3.4", "globby": "^10.0.2", "ts-node": "^8.10.2", - "winston": "^3.5.1" + "winston": "^3.5.1", + "zod": "^3.22.4" }, "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/options": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", - "@dotcom-tool-kit/wait-for-ok": "^3.2.0", - "cosmiconfig": "^7.0.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/config": "2.0.0-beta.0", + "@dotcom-tool-kit/conflict": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", + "@dotcom-tool-kit/validated": "2.0.0-beta.0", + "@dotcom-tool-kit/wait-for-ok": "4.0.0-beta.0", + "endent": "^2.1.0", "lodash": "^4.17.21", "minimist": "^1.2.5", - "resolve-from": "^5.0.0", "tslib": "^2.3.1", - "yaml": "^1.10.2", - "zod-validation-error": "^0.3.0" + "yaml": "^2.4.1", + "zod-validation-error": "^2.1.0" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "files": [ diff --git a/core/cli/src/config.ts b/core/cli/src/config.ts index d3d284f15..5d8ef8812 100644 --- a/core/cli/src/config.ts +++ b/core/cli/src/config.ts @@ -1,97 +1,69 @@ import path from 'path' import type { Logger } from 'winston' -import type { HookTask } from './hook' -import { loadPlugin, resolvePlugin } from './plugin' -import { Conflict, findConflicts, withoutConflicts, isConflict } from './conflict' -import { ToolKitConflictError, ToolKitError } from '@dotcom-tool-kit/error' -import { TaskClass, Hook, mapValidated, Plugin, reduceValidated, Validated } from '@dotcom-tool-kit/types' -import { Options as SchemaOptions, Schemas } from '@dotcom-tool-kit/types/lib/schema' +import { loadPlugin, resolvePlugin, resolvePluginOptions } from './plugin' +import { + findConflicts, + withoutConflicts, + isConflict, + findConflictingEntries +} from '@dotcom-tool-kit/conflict' +import { ToolKitError, ToolKitConflictError } from '@dotcom-tool-kit/error' +import { RawConfig, ValidConfig, ValidPluginsConfig } from '@dotcom-tool-kit/config' import { - InvalidOption, formatTaskConflicts, - formatUndefinedHookTasks, - formatUnusedOptions, - formatHookTaskConflicts, + formatUnusedPluginOptions, + formatCommandTaskConflicts, formatHookConflicts, - formatOptionConflicts, - formatUninstalledHooks, + formatPluginOptionConflicts, formatMissingTasks, - formatInvalidOptions + formatInvalidPluginOptions, + formatTaskOptionConflicts, + formatUnusedTaskOptions } from './messages' - -export interface PluginOptions { - options: Record - plugin: Plugin - forPlugin: Plugin -} - -export interface RawConfig { - root: string - plugins: { [id: string]: Validated } - resolvedPlugins: Set - tasks: { [id: string]: TaskClass | Conflict } - hookTasks: { [id: string]: HookTask | Conflict } - options: { [id: string]: PluginOptions | Conflict | undefined } - hooks: { [id: string]: Hook | Conflict> } -} - -export type ValidPluginsConfig = Omit & { - plugins: { [id: string]: Plugin } -} - -export type ValidPluginOptions = Omit & { - options: SchemaOptions[Id] -} - -export type ValidOptions = { - [Id in keyof SchemaOptions]: ValidPluginOptions -} - -export type ValidConfig = Omit & { - tasks: { [id: string]: TaskClass } - hookTasks: { [id: string]: HookTask } - options: ValidOptions - hooks: { [id: string]: Hook } -} +import { validatePlugins } from './config/validate-plugins' +import { substituteOptionTags, validatePluginOptions } from './plugin/options' const coreRoot = path.resolve(__dirname, '../') export const createConfig = (): RawConfig => ({ root: coreRoot, plugins: {}, - resolvedPlugins: new Set(), + resolutionTrackers: { + resolvedPluginOptions: new Set(), + substitutedPlugins: new Set(), + resolvedPlugins: new Set(), + reducedInstallationPlugins: new Set() + }, tasks: {}, - hookTasks: {}, - options: {}, - hooks: {} + commandTasks: {}, + pluginOptions: {}, + taskOptions: {}, + hooks: {}, + inits: [], + hookManagedFiles: new Set() }) -async function asyncFilter(items: T[], predicate: (item: T) => Promise): Promise { - const results = await Promise.all(items.map(async (item) => ({ item, keep: await predicate(item) }))) - - return results.filter(({ keep }) => keep).map(({ item }) => item) -} - -export function validateConfig(config: ValidPluginsConfig, logger: Logger): ValidConfig { +export function validateConfig(config: ValidPluginsConfig): ValidConfig { const validConfig = config as ValidConfig - const hookTaskConflicts = findConflicts(Object.values(config.hookTasks)) - const hookConflicts = findConflicts(Object.values(config.hooks)) - const taskConflicts = findConflicts(Object.values(config.tasks)) - const optionConflicts = findConflicts(Object.values(config.options)) + const commandTaskConflicts = findConflicts(Object.values(config.commandTasks)) + const hookConflicts = findConflictingEntries(config.hooks) + const taskConflicts = findConflictingEntries(config.tasks) + const pluginOptionConflicts = findConflicts(Object.values(config.pluginOptions)) + const taskOptionConflicts = findConflicts(Object.values(config.taskOptions)) - const definedHookTaskConflicts = hookTaskConflicts.filter((conflict) => { + const definedCommandTaskConflicts = commandTaskConflicts.filter((conflict) => { return conflict.conflicting[0].id in config.hooks }) let shouldThrow = false const error = new ToolKitConflictError( 'There are problems with your Tool Kit configuration.', - hookTaskConflicts.map((conflict) => ({ - hook: conflict.conflicting[0].id, - conflictingTasks: conflict.conflicting.flatMap((hook) => - hook.tasks.map((task) => ({ task, plugin: hook.plugin.id })) + commandTaskConflicts.map((conflict) => ({ + command: conflict.conflicting[0].id, + conflictingTasks: conflict.conflicting.flatMap((command) => + command.tasks.map((task) => ({ task: task.task, plugin: command.plugin.id })) ) })) ) @@ -99,9 +71,10 @@ export function validateConfig(config: ValidPluginsConfig, logger: Logger): Vali if ( hookConflicts.length > 0 || - definedHookTaskConflicts.length > 0 || + definedCommandTaskConflicts.length > 0 || taskConflicts.length > 0 || - optionConflicts.length > 0 + pluginOptionConflicts.length > 0 || + taskOptionConflicts.length > 0 ) { shouldThrow = true @@ -109,87 +82,52 @@ export function validateConfig(config: ValidPluginsConfig, logger: Logger): Vali error.details += formatHookConflicts(hookConflicts) } - if (definedHookTaskConflicts.length) { - error.details += formatHookTaskConflicts(definedHookTaskConflicts) + if (definedCommandTaskConflicts.length) { + error.details += formatCommandTaskConflicts(definedCommandTaskConflicts) } if (taskConflicts.length) { error.details += formatTaskConflicts(taskConflicts) } - if (optionConflicts.length) { - error.details += formatOptionConflicts(optionConflicts) + if (pluginOptionConflicts.length) { + error.details += formatPluginOptionConflicts(pluginOptionConflicts) } - } - - const configuredHookTasks = withoutConflicts(Object.values(config.hookTasks)) - const definedHookIds = new Set(Object.keys(config.hooks)) - const undefinedHookTasks = configuredHookTasks.filter((hookTask) => { - // we only care about undefined hooks that were configured by the app, not default config from plugins - const fromApp = hookTask.plugin.root === process.cwd() - const hookDefined = definedHookIds.has(hookTask.id) - return fromApp && !hookDefined - }) - if (undefinedHookTasks.length > 0) { - shouldThrow = true - error.details += formatUndefinedHookTasks(undefinedHookTasks, Array.from(definedHookIds)) + if (taskOptionConflicts.length) { + error.details += formatTaskOptionConflicts(taskOptionConflicts) + } } - const invalidOptions: InvalidOption[] = [] - for (const [id, plugin] of Object.entries(config.plugins)) { - const pluginId = id as keyof SchemaOptions - const pluginOptions = config.options[pluginId] - if (pluginOptions && isConflict(pluginOptions)) { - continue - } + const unusedPluginOptions = Object.entries(config.pluginOptions) + .filter( + ([, option]) => + option && !isConflict(option) && !option.forPlugin && option.plugin.root === process.cwd() + ) + .map(([id]) => id) - const pluginSchema = Schemas[pluginId] - if (!pluginSchema) { - logger.silly(`skipping validation of ${pluginId} plugin as no schema can be found`) - continue - } - const result = pluginSchema.safeParse(pluginOptions?.options ?? {}) - if (result.success) { - // Set up options entry for plugins that don't have options specified - // explicitly. They could still have default options that are set by zod. - if (!pluginOptions) { - // TypeScript struggles with this type as it sees one side as - // `Foo` and the other as `Foo | Foo | Foo` for - // some reason (something to do with the record indexing) and it can't - // unify them. But they are equivalent so let's force it with a cast. - config.options[pluginId] = { - options: result.data, - plugin: config.plugins['app root'], - forPlugin: plugin - } as any // eslint-disable-line @typescript-eslint/no-explicit-any - } else { - pluginOptions.options = result.data - } - } else { - invalidOptions.push([id, result.error]) - } - } - if (invalidOptions.length > 0) { + if (unusedPluginOptions.length > 0) { shouldThrow = true - error.details += formatInvalidOptions(invalidOptions) + error.details += formatUnusedPluginOptions(unusedPluginOptions, Object.keys(config.plugins)) } - const unusedOptions = Object.entries(config.options) + const unusedTaskOptions = Object.entries(config.taskOptions) .filter( - ([, option]) => - option && !isConflict(option) && !option.forPlugin && option.plugin.root === process.cwd() + ([, option]) => option && !isConflict(option) && !option.task && option.plugin.root === process.cwd() ) .map(([id]) => id) - if (unusedOptions.length > 0) { + + if (unusedTaskOptions.length > 0) { shouldThrow = true - error.details += formatUnusedOptions(unusedOptions, Object.keys(config.plugins)) + error.details += formatUnusedTaskOptions(unusedTaskOptions, Object.keys(config.tasks)) } - const missingTasks = configuredHookTasks - .map((hook) => ({ - hook, - tasks: hook.tasks.filter((id) => !config.tasks[id]) + const configuredCommandTasks = withoutConflicts(Object.values(config.commandTasks)) + + const missingTasks = configuredCommandTasks + .map((command) => ({ + command, + tasks: command.tasks.filter((task) => !config.tasks[task.task]) })) .filter(({ tasks }) => tasks.length > 0) @@ -205,26 +143,6 @@ export function validateConfig(config: ValidPluginsConfig, logger: Logger): Vali return validConfig } -export function validatePlugins(config: RawConfig): Validated { - const validatedPlugins = reduceValidated( - Object.entries(config.plugins).map(([id, plugin]) => mapValidated(plugin, (p) => [id, p] as const)) - ) - return mapValidated(validatedPlugins, (plugins) => ({ ...config, plugins: Object.fromEntries(plugins) })) -} - -export async function checkInstall(config: ValidConfig): Promise { - const definedHooks = withoutConflicts(Object.values(config.hooks)) - const uninstalledHooks = await asyncFilter(definedHooks, async (hook) => { - return !(await hook.check()) - }) - - if (uninstalledHooks.length > 0) { - const error = new ToolKitError('There are problems with your Tool Kit installation.') - error.details = formatUninstalledHooks(uninstalledHooks) - throw error - } -} - export function loadConfig(logger: Logger, options?: { validate?: true }): Promise export function loadConfig(logger: Logger, options?: { validate?: false }): Promise @@ -233,24 +151,23 @@ export async function loadConfig(logger: Logger, { validate = true } = {}): Prom // start loading config and child plugins, starting from the consumer app directory const rootPlugin = await loadPlugin('app root', config, logger) - if (!rootPlugin.valid) { - const error = new ToolKitError('root plugin was not valid!') - error.details = rootPlugin.reasons.join('\n\n') - throw error - } - const validRootPlugin = rootPlugin.value + const validRootPlugin = rootPlugin.unwrap('root plugin was not valid!') const validatedPluginConfig = validatePlugins(config) + const validPluginConfig = validatedPluginConfig.unwrap('config was not valid!') - if (!validatedPluginConfig.valid) { - const error = new ToolKitError('config was not valid!') - error.details = validatedPluginConfig.reasons.join('\n\n') + // collate root plugin and descendent hooks, options etc into config + // start with options so we can substitute resolved values into other parts + // of the config + resolvePluginOptions(validRootPlugin, validPluginConfig) + const invalidOptions = validatePluginOptions(logger, validPluginConfig) + if (invalidOptions.length > 0 && validate) { + const error = new ToolKitError('There are problems with your plugin options.') + error.details = formatInvalidPluginOptions(invalidOptions) throw error } - const validPluginConfig = validatedPluginConfig.value - - // collate root plugin and descendent hooks, options etc into config + substituteOptionTags(validRootPlugin, validPluginConfig) resolvePlugin(validRootPlugin, validPluginConfig, logger) - return validate ? validateConfig(validPluginConfig, logger) : config + return validate ? validateConfig(validPluginConfig) : config } diff --git a/core/cli/src/config/hash.ts b/core/cli/src/config/hash.ts new file mode 100644 index 000000000..151f24d22 --- /dev/null +++ b/core/cli/src/config/hash.ts @@ -0,0 +1,47 @@ +import { ValidConfig } from '@dotcom-tool-kit/config' +import { readState, writeState } from '@dotcom-tool-kit/state' +import { createHash } from 'node:crypto' +import { readFile } from 'node:fs/promises' +import { Logger } from 'winston' + +export async function fileHash(path: string): Promise { + const hashFunc = createHash('sha512') + try { + hashFunc.update(await readFile(path)) + return hashFunc.digest('base64') + } catch (error) { + if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') { + return 'n/a' + } else { + throw error + } + } +} + +export async function updateHashes(config: ValidConfig): Promise { + const hashes = Object.fromEntries( + await Promise.all( + ['.toolkitrc.yml', ...config.hookManagedFiles].map(async (path) => [path, await fileHash(path)]) + ) + ) + writeState('install', hashes) +} + +export async function hasConfigChanged(logger: Logger, config: ValidConfig): Promise { + const hashes = readState('install') + + if (!hashes) { + return true + } + + for (const path of ['.toolkitrc.yml', ...config.hookManagedFiles]) { + const newHash = await fileHash(path) + const prevHash = hashes[path] + + if (newHash !== prevHash) { + logger.debug(`hash for path ${path} has changed, running hook checks`) + return true + } + } + return false +} diff --git a/core/cli/src/config/validate-plugins.ts b/core/cli/src/config/validate-plugins.ts new file mode 100644 index 000000000..89e91b536 --- /dev/null +++ b/core/cli/src/config/validate-plugins.ts @@ -0,0 +1,9 @@ +import type { RawConfig, ValidPluginsConfig } from '@dotcom-tool-kit/config' +import { Validated, reduceValidated } from '@dotcom-tool-kit/validated' + +export function validatePlugins(config: RawConfig): Validated { + const validatedPlugins = reduceValidated( + Object.entries(config.plugins).map(([id, plugin]) => plugin.map((p) => [id, p] as const)) + ) + return validatedPlugins.map((plugins) => ({ ...config, plugins: Object.fromEntries(plugins) })) +} diff --git a/core/cli/src/fetch.ts b/core/cli/src/fetch.ts new file mode 100644 index 000000000..7de0cedf6 --- /dev/null +++ b/core/cli/src/fetch.ts @@ -0,0 +1,17 @@ +import { getOptions } from '@dotcom-tool-kit/options' + +// function that plugins can check if they need to implement their own logic to +// disable Node 18's native fetch +export const shouldDisableNativeFetch = (): boolean => { + // disable Node 18's native fetch if the Node runtime supports it (older + // runtimes don't support the flag, implying they also don't use native + // fetch) and the user hasn't opted out of the behaviour + return ( + /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- + * the root plugin has default options and it always exists so is always + * defined + **/ + !getOptions('app root')!.allowNativeFetch && + process.allowedNodeEnvironmentFlags.has('--no-experimental-fetch') + ) +} diff --git a/core/cli/src/help.ts b/core/cli/src/help.ts index da0a8424f..744bc8f2b 100644 --- a/core/cli/src/help.ts +++ b/core/cli/src/help.ts @@ -1,71 +1,124 @@ -import { checkInstall, loadConfig } from './config' +import { loadConfig } from './config' import { OptionKey, setOptions } from '@dotcom-tool-kit/options' -import { styles } from '@dotcom-tool-kit/logger' +import { styles as s } from '@dotcom-tool-kit/logger' import type { Logger } from 'winston' +import YAML from 'yaml' +import $t from 'endent' +import { CommandTask, OptionsForTask } from '@dotcom-tool-kit/plugin' +import { ValidConfig } from '@dotcom-tool-kit/config' -export default async function showHelp(logger: Logger, hooks: string[]): Promise { - const config = await loadConfig(logger) +const toolKitIntro = s.box( + $t` + Tool Kit is modern, maintainable & modular developer tooling for FT.com projects. + ${s.URL('https://github.com/financial-times/dotcom-tool-kit')} + `, + { title: `🧰 ${s.title(`welcome to ${s.app('Tool Kit')}!`)}` } +) - if (hooks.length === 0) { - hooks = Object.keys(config.hooks).sort() - } +const formatTask = ({ task, options }: OptionsForTask) => $t` + ${s.task(task)}${ + Object.keys(options).length > 0 + ? ` ${s.dim('with options:')} + ${YAML.stringify(options).trim()}` + : '' +}` - for (const pluginOptions of Object.values(config.options)) { - if (pluginOptions.forPlugin) { - setOptions(pluginOptions.forPlugin.id as OptionKey, pluginOptions.options) +const formatCommandTasks = (config: ValidConfig, commands: string[]) => + s.box( + $t` + ${s.help( + `${s.command('commands')} run Tool Kit tasks with ${s.code( + 'npx dotcom-tool-kit $command' + )}, or via configuration installed by hooks in your repository.` + )} + ${commands + .filter((command) => config.commandTasks[command]) + .map((command) => formatCommandTask(command, config.commandTasks[command])) + .join('\n')} + +`, + { + title: '⛭ ' + s.title('available commands') } - } + ) - await checkInstall(config) +const formatCommandTask = (command: string, { tasks, plugin }: CommandTask) => $t` + ${s.groupHeader(s.command(command))} + ${ + tasks.length + ? $t` + ${s.info(`${plugin.id !== 'app root' ? `from plugin ${s.plugin(plugin.id)}. ` : ''}runs tasks:`)} + ${tasks.map((task) => ` - ${formatTask(task)}`).join('\n')} + ` + : s.warning(`no tasks configured to run for ${s.command(command)}`) + } +` - const missingHooks = hooks.filter((hook) => !config.hooks[hook]) +const formatHooks = (config: ValidConfig) => + s.box( + $t` + ${s.help( + `${s.hook('hooks')} manage configuration files in your repository, for running Tool Kit commands.` + )} + ${Object.entries(config.hooks) + .map(([hook, entryPoint]) => { + const managesFiles = entryPoint.plugin.rcFile?.installs[hook].managesFiles ?? [] + return $t` + ${s.groupHeader(s.hook(hook))} + ${s.info($t` + from plugin ${s.plugin(entryPoint.plugin.id)} + `)} + ${managesFiles.length ? 'manages files:' : ''} + ${managesFiles.map((file) => ` - ${s.filepath(file)}`).join('\n')} + ` + }) + .join('\n')} +`, + { title: s.title('🎣 installed hooks') } + ) - logger.info(` -🧰 ${styles.title(`welcome to ${styles.app('Tool Kit')}!`)} +export default async function showHelp(logger: Logger, commands: string[]): Promise { + const config = await loadConfig(logger) + const printAllCommands = commands.length === 0 -Tool Kit is modern, maintainable & modular developer tooling for FT.com projects. + if (printAllCommands) { + commands = Object.keys(config.commandTasks).sort() + } -${styles.URL('https://github.com/financial-times/dotcom-tool-kit')} + for (const pluginOptions of Object.values(config.pluginOptions)) { + if (pluginOptions.forPlugin) { + setOptions(pluginOptions.forPlugin.id as OptionKey, pluginOptions.options) + } + } -${styles.ruler()} -${ - Object.keys(config.hooks).length === 0 - ? `there are no hooks available. you'll need to install plugins that define hooks to be able to run Tool Kit tasks.` - : styles.dim( - hooks.length === 0 - ? 'available hooks' - : `help for ${hooks.length - missingHooks.length} ${ - hooks.length - missingHooks.length > 1 ? 'hooks' : 'hook' - }` - ) -}: -`) + logger.info(toolKitIntro) - for (const hook of hooks) { - const Hook = config.hooks[hook] + const definedCommands = commands.filter((command) => config.commandTasks[command]) + const missingCommands = commands.filter((command) => !config.commandTasks[command]) - if (Hook) { - const tasks = config.hookTasks[hook] - /* eslint-disable @typescript-eslint/no-explicit-any -- Object.constructor does not consider static properties */ - logger.info(`${styles.heading(hook)} -${(Hook.constructor as any).description ? (Hook.constructor as any).description + '\n' : ''} -${ - tasks && tasks.tasks.length - ? `runs ${tasks.tasks.length > 1 ? 'these tasks' : 'this task'}: -${tasks.tasks - .map((task) => `- ${styles.task(task)} ${styles.dim(config.tasks[task].description)}`) - .join('\n')}` - : styles.dim('no tasks configured to run on this hook.') -} -${styles.ruler()} -`) - /*eslint-enable @typescript-eslint/no-explicit-any */ - } + if (printAllCommands && Object.keys(config.hooks).length) { + logger.info(formatHooks(config)) } - if (missingHooks.length) { + if (Object.keys(config.commandTasks).length === 0) { + logger.warn( + s.warning($t` + there are no commands available. add some commands by defining them in your ${s.filepath( + '.toolkitrc.yml' + )} or installing plugins that define commands. + `) + ) + } else if (definedCommands.length > 0) { + logger.info(formatCommandTasks(config, definedCommands)) + } else if (missingCommands.length) { logger.warn( - `no such ${missingHooks.length > 1 ? 'hooks' : 'hook'} ${missingHooks.map(styles.hook).join(', ')}` + s.error($t` + no such ${missingCommands.length > 1 ? 'commands' : 'command'} ${missingCommands + .map((id) => s.command(id)) + .join(', ')} + `) ) } + + logger.info('\n') } diff --git a/core/cli/src/hook.ts b/core/cli/src/hook.ts deleted file mode 100644 index 733138ec3..000000000 --- a/core/cli/src/hook.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Plugin } from '@dotcom-tool-kit/types' - -export interface HookTask { - id: string - plugin: Plugin - tasks: string[] -} diff --git a/core/cli/src/index.ts b/core/cli/src/index.ts index 304a2e03e..bf65c2bf2 100644 --- a/core/cli/src/index.ts +++ b/core/cli/src/index.ts @@ -1,117 +1,10 @@ -import { ToolKitError } from '@dotcom-tool-kit/error' -import { checkInstall, loadConfig } from './config' -import { OptionKey, getOptions, setOptions } from '@dotcom-tool-kit/options' -import { styles } from '@dotcom-tool-kit/logger' +import { loadConfig } from './config' import type { Logger } from 'winston' +import util from 'util' import { formatPluginTree } from './messages' -type ErrorSummary = { - hook: string - task: string - error: Error -} - -// function that plugins can check if they need to implement their own logic to -// disable Node 18's native fetch -export const shouldDisableNativeFetch = (): boolean => { - // disable Node 18's native fetch if the Node runtime supports it (older - // runtimes don't support the flag, implying they also don't use native - // fetch) and the user hasn't opted out of the behaviour - return ( - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- - * the root plugin has default options and it always exists so is always - * defined - **/ - !getOptions('app root')!.allowNativeFetch && - process.allowedNodeEnvironmentFlags.has('--no-experimental-fetch') - ) -} - -export async function runTasks(logger: Logger, hooks: string[], files?: string[]): Promise { - const config = await loadConfig(logger) - - const availableHooks = Object.keys(config.hooks) - .sort() - .map((id) => `- ${id}`) - .join('\n') - - const missingHooks = hooks.filter((id) => !config.hooks[id]) - - if (missingHooks.length > 0) { - const error = new ToolKitError(`hooks ${missingHooks} do not exist`) - error.details = `maybe you need to install a plugin to handle these hooks, or configure them in your Tool Kit configuration. - -hooks that are available are: -${availableHooks}` - throw error - } - - for (const pluginOptions of Object.values(config.options)) { - if (pluginOptions.forPlugin) { - setOptions(pluginOptions.forPlugin.id as OptionKey, pluginOptions.options) - } - } - - await checkInstall(config) - - if (shouldDisableNativeFetch()) { - process.execArgv.push('--no-experimental-fetch') - } - - for (const hook of hooks) { - const errors: ErrorSummary[] = [] - - if (!config.hookTasks[hook]) { - logger.warn(`no task configured for ${hook}: skipping assignment...`) - continue - } - const assignment = config.hookTasks[hook] - - for (const id of assignment.tasks) { - const Task = config.tasks[id] - const options = Task.plugin ? getOptions(Task.plugin.id as OptionKey) : {} - - /* eslint-disable-next-line @typescript-eslint/no-explicit-any -- - * `Task` is an abstract class. Here we know it's a concrete subclass - * but typescript doesn't, so cast it to any. - **/ - const task = new (Task as any)(logger, options) - - logger.info(styles.taskHeader(`running ${styles.task(id)} task`)) - try { - await task.run(files) - } catch (error) { - // allow subsequent hook tasks to run on error - errors.push({ - hook, - task: id, - error: error as Error - }) - } - } - - if (errors.length > 0) { - const error = new ToolKitError(`error running tasks for ${styles.hook(hook)}`) - error.details = errors - .map( - ({ task, error }) => - `${styles.heading(`${styles.task(task)}:`)} - -${error.message}${ - error instanceof ToolKitError - ? ` - -${error.details}` - : '' - }` - ) - .join(`${styles.dim(styles.ruler())}\n`) - - error.exitCode = errors.length + 1 - throw error - } - } -} +export { runCommands } from './tasks' +export { shouldDisableNativeFetch } from './fetch' export async function listPlugins(logger: Logger): Promise { const config = await loadConfig(logger, { validate: false }) @@ -122,5 +15,8 @@ export async function listPlugins(logger: Logger): Promise { } } -export { default as showHelp } from './help' -export { default as installHooks } from './install' +export async function printConfig(logger: Logger): Promise { + const config = await loadConfig(logger, { validate: false }) + + logger.info(util.inspect(config, { depth: null, colors: true })) +} diff --git a/core/cli/src/init.ts b/core/cli/src/init.ts new file mode 100644 index 000000000..26b116da0 --- /dev/null +++ b/core/cli/src/init.ts @@ -0,0 +1,20 @@ +import type { ValidConfig } from '@dotcom-tool-kit/config' +import { Init, InitClass } from '@dotcom-tool-kit/base' +import { Validated, reduceValidated } from '@dotcom-tool-kit/validated' +import type { Logger } from 'winston' +import { importEntryPoint } from './plugin/entry-point' + +const loadInitEntrypoints = async (logger: Logger, config: ValidConfig): Promise> => { + const initClassResults = reduceValidated( + await Promise.all(config.inits.map((entryPoint) => importEntryPoint(Init as InitClass, entryPoint))) + ) + + return initClassResults.map((initClasses) => initClasses.map((initClass) => new initClass(logger))) +} + +export async function runInit(logger: Logger, config: ValidConfig): Promise { + const initResults = await loadInitEntrypoints(logger, config) + const inits = initResults.unwrap('plugin initialisation classes were invalid!') + + await Promise.all(inits.map(async (init) => init.init())) +} diff --git a/core/cli/src/install.ts b/core/cli/src/install.ts index 555c86085..2dfb25859 100644 --- a/core/cli/src/install.ts +++ b/core/cli/src/install.ts @@ -1,11 +1,21 @@ +import type { z } from 'zod' import { ToolKitError } from '@dotcom-tool-kit/error' import { OptionKey, setOptions } from '@dotcom-tool-kit/options' import groupBy from 'lodash/groupBy' import type { Logger } from 'winston' -import { loadConfig, ValidConfig } from './config' -import { postInstall } from './postInstall' +import { loadConfig } from './config' +import { hasConfigChanged, updateHashes } from './config/hash' +import type { ValidConfig } from '@dotcom-tool-kit/config' +import { Hook, HookClass } from '@dotcom-tool-kit/base' +import { Validated, invalid, reduceValidated, valid } from '@dotcom-tool-kit/validated' +import { reducePluginHookInstallations } from './plugin/reduce-installations' +import { findConflicts, withoutConflicts } from '@dotcom-tool-kit/conflict' +import { HookOptions, HookSchemas } from '@dotcom-tool-kit/schemas' +import { formatUninstalledHooks } from './messages' +import { importEntryPoint } from './plugin/entry-point' +import { runInit } from './init' -// implementation of the Array.every method that supports asynchronous predicates +// implementation of the Array#every method that supports asynchronous predicates async function asyncEvery(arr: T[], pred: (x: T) => Promise): Promise { for (const val of arr) { if (!(await pred(val))) { @@ -15,23 +25,106 @@ async function asyncEvery(arr: T[], pred: (x: T) => Promise): Promis return true } +// implementation of the Array#filter method that supports asynchronous predicates +async function asyncFilter(items: T[], predicate: (item: T) => Promise): Promise { + const results = await Promise.all(items.map(async (item) => ({ item, keep: await predicate(item) }))) + + return results.filter(({ keep }) => keep).map(({ item }) => item) +} + +const loadHookEntrypoints = async ( + logger: Logger, + config: ValidConfig +): Promise>> => { + const hookResultEntries = reduceValidated( + await Promise.all( + Object.entries(config.hooks).map(async ([hookName, entryPoint]) => { + const hookResult = await importEntryPoint(Hook, entryPoint) + return hookResult.map((hookClass) => [hookName, hookClass as HookClass] as const) + }) + ) + ) + + return hookResultEntries.map((hookEntries) => Object.fromEntries(hookEntries)) +} + +export const loadHookInstallations = async ( + logger: Logger, + config: ValidConfig +): Promise[]>> => { + const hookClassResults = await loadHookEntrypoints(logger, config) + const installationResults = await hookClassResults + .map((hookClasses) => + reducePluginHookInstallations(logger, config, hookClasses, config.plugins['app root']) + ) + .awaitValue() + + const installationsWithoutConflicts = installationResults.flatMap((installations) => { + const conflicts = findConflicts(installations) + + if (conflicts.length) { + return invalid<[]>( + conflicts.map( + // TODO:20240429:IM format a more helpful error message here + (conflict) => + `conflicts between ${conflict.conflicting.map((installation) => installation.forHook).join(', ')}` + ) + ) + } + + return valid(withoutConflicts(installations)) + }) + + return installationsWithoutConflicts.map((installations) => { + return installations.map(({ hookConstructor, forHook, options }) => { + const schema = HookSchemas[forHook as keyof HookOptions] + const parsedOptions = schema ? schema.parse(options) : {} + return new hookConstructor(logger, forHook, parsedOptions) + }) + }) +} + +export async function checkInstall(logger: Logger, config: ValidConfig): Promise { + if (!(await hasConfigChanged(logger, config))) { + return + } + + const hooks = (await loadHookInstallations(logger, config)).unwrap('hooks are invalid') + + const uninstalledHooks = await asyncFilter(hooks, async (hook) => { + return !(await hook.isInstalled()) + }) + + if (uninstalledHooks.length > 0) { + const error = new ToolKitError('There are problems with your Tool Kit installation.') + error.details = formatUninstalledHooks(uninstalledHooks) + throw error + } + + await updateHashes(config) +} + export default async function installHooks(logger: Logger): Promise { const config = await loadConfig(logger) - for (const pluginOptions of Object.values(config.options)) { + for (const pluginOptions of Object.values(config.pluginOptions)) { if (pluginOptions.forPlugin) { setOptions(pluginOptions.forPlugin.id as OptionKey, pluginOptions.options) } } + await runInit(logger, config) + const errors: Error[] = [] + const hooks = (await loadHookInstallations(logger, config)).unwrap('hooks are invalid') + // group hooks without an installGroup separately so that their check() // method runs independently - const groups = groupBy(config.hooks, (hook) => hook.installGroup ?? '__' + hook.id) + const groups = groupBy(hooks, (hook) => hook.installGroup ?? '__' + hook.id) for (const group of Object.values(groups)) { try { - const allChecksPassed = await asyncEvery(group, (hook) => hook.check()) - if (!allChecksPassed) { + const allHooksInstalled = await asyncEvery(group, (hook) => hook.isInstalled()) + if (!allHooksInstalled) { let state = undefined for (const hook of group) { state = await hook.install(state) @@ -57,19 +150,13 @@ export default async function installHooks(logger: Logger): Promise } } - // HACK: achieve backwards compatibility with older versions of the circleci - // plugin that required a postinstall function to run instead of the new - // commitInstall method. remove in major update of cli. - const usesNewCircleCIGroup = Object.keys(groups).includes('circleci') - if (!usesNewCircleCIGroup) { - await postInstall(logger) - } - if (errors.length) { const error = new ToolKitError('could not automatically install hooks:') error.details = errors.map((error) => `- ${error.message}`).join('\n') throw error } + await updateHashes(config) + return config } diff --git a/core/cli/src/messages.ts b/core/cli/src/messages.ts index f334d4b65..b6309e86f 100644 --- a/core/cli/src/messages.ts +++ b/core/cli/src/messages.ts @@ -1,70 +1,73 @@ -import type { PluginOptions } from './config' -import type { Conflict } from './conflict' -import type { HookTask } from './hook' import { styles as s, styles } from '@dotcom-tool-kit/logger' -import type { Plugin, Hook, TaskClass } from '@dotcom-tool-kit/types' +import type { Hook } from '@dotcom-tool-kit/base' +import type { + CommandTask, + EntryPoint, + Plugin, + OptionsForPlugin, + OptionsForTask +} from '@dotcom-tool-kit/plugin' import type { z } from 'zod' import { fromZodError } from 'zod-validation-error' +import type { Conflict } from '@dotcom-tool-kit/conflict' -const formatTaskConflict = (conflict: Conflict): string => - `- ${s.task(conflict.conflicting[0].id || 'unknown task')} ${s.dim( - 'from plugins' - )} ${conflict.conflicting - .map((task) => s.plugin(task.plugin ? task.plugin.id : 'unknown plugin')) +const formatTaskConflict = ([key, conflict]: [string, Conflict]): string => + `- ${s.task(key ?? 'unknown task')} ${s.dim('from plugins')} ${conflict.conflicting + .map((entryPoint) => s.plugin(entryPoint.plugin.id ?? 'unknown plugin')) .join(s.dim(', '))}` -export const formatTaskConflicts = (conflicts: Conflict[]): string => `${s.heading( +export const formatTaskConflicts = (conflicts: [string, Conflict][]): string => `${s.heading( 'There are multiple plugins that include the same tasks' )}: ${conflicts.map(formatTaskConflict).join('\n')} You must resolve this conflict by removing all but one of these plugins.` -const formatHookConflict = (conflict: Conflict>): string => - `- ${s.hook(conflict.conflicting[0].id || 'unknown event')} ${s.dim( - 'from plugins' - )} ${conflict.conflicting - .map((task) => s.plugin(task.plugin ? task.plugin.id : 'unknown plugin')) +const formatHookConflict = ([key, conflict]: [string, Conflict]): string => + `- ${s.hook(key ?? 'unknown hook')} ${s.dim('from plugins')} ${conflict.conflicting + .map((entryPoint) => s.plugin(entryPoint.plugin.id ?? 'unknown plugin')) .join(s.dim(', '))}` -export const formatHookConflicts = (conflicts: Conflict>[]): string => `${s.heading( +export const formatHookConflicts = (conflicts: [string, Conflict][]): string => `${s.heading( 'There are multiple plugins that include the same hooks' )}: ${conflicts.map(formatHookConflict).join('\n')} You must resolve this conflict by removing all but one of these plugins.` -const formatHookTaskConflict = (conflict: Conflict): string => `${s.hook( +const formatCommandTaskConflict = (conflict: Conflict): string => `${s.hook( conflict.conflicting[0].id )}: ${conflict.conflicting .map( - (hook) => - `- ${hook.tasks.map(s.task).join(s.dim(', '))} ${s.dim('by plugin')} ${s.plugin(hook.plugin.id)}` + (command) => + `- ${command.tasks.map((task) => s.task(task.task)).join(s.dim(', '))} ${s.dim('by plugin')} ${s.plugin( + command.plugin.id + )}` ) .join('\n')} ` -export const formatHookTaskConflicts = (conflicts: Conflict[]): string => `${s.heading( - 'These hooks are configured to run different tasks by multiple plugins' +export const formatCommandTaskConflicts = (conflicts: Conflict[]): string => `${s.heading( + 'These commands are configured to run different tasks by multiple plugins' )}: -${conflicts.map(formatHookTaskConflict).join('\n')} -You must resolve this conflict by explicitly configuring which task to run for these hooks. See ${s.URL( +${conflicts.map(formatCommandTaskConflict).join('\n')} +You must resolve this conflict by explicitly configuring which task to run for these commands. See ${s.URL( 'https://github.com/financial-times/dotcom-tool-kit/tree/main/docs/resolving-hook-conflicts.md' )} for more details. ` -const formatOptionConflict = (conflict: Conflict): string => `${s.plugin( +const formatPluginOptionConflict = (conflict: Conflict): string => `${s.plugin( conflict.conflicting[0].forPlugin.id )}, configured by: ${conflict.conflicting.map((option) => `- ${s.plugin(option.plugin.id)}`)}` -export const formatOptionConflicts = (conflicts: Conflict[]): string => `${s.heading( +export const formatPluginOptionConflicts = (conflicts: Conflict[]): string => `${s.heading( 'These plugins have conflicting options' )}: -${conflicts.map(formatOptionConflict).join('\n')} +${conflicts.map(formatPluginOptionConflict).join('\n')} You must resolve this conflict by providing options in your app's Tool Kit configuration for these plugins, or installing a use-case plugin that provides these options. See ${s.URL( 'https://github.com/financial-times/dotcom-tool-kit/tree/main/readme.md#options' @@ -72,40 +75,41 @@ You must resolve this conflict by providing options in your app's Tool Kit confi ` -const formatPlugin = (plugin: Plugin): string => - plugin.id === 'app root' ? s.app('your app') : `plugin ${s.plugin(plugin.id)}` +const formatTaskOptionConflict = (conflict: Conflict): string => `${s.task( + conflict.conflicting[0].task +)}, configured by: +${conflict.conflicting.map((option) => `- ${s.plugin(option.plugin.id)}`)}` -// TODO text similarity "did you mean...?" -export const formatUndefinedHookTasks = ( - undefinedHooks: HookTask[], - definedHooks: string[] -): string => `Hooks must be defined by a plugin before you can configure a task to run for them. In your Tool Kit configuration you've configured hooks that aren't defined: +export const formatTaskOptionConflicts = (conflicts: Conflict[]): string => `${s.heading( + 'These tasks have conflicting options' +)}: -${undefinedHooks.map((hook) => `- ${s.hook(hook.id)}`).join('\n')} +${conflicts.map(formatTaskOptionConflict).join('\n')} -They could be misspelt, or defined by a Tool Kit plugin that isn't installed in this app. +You must resolve this conflict by providing options in your app's Tool Kit configuration for these tasks, or installing a use-case plugin that provides these options. See ${s.URL( + 'https://github.com/financial-times/dotcom-tool-kit/tree/main/readme.md#options' +)} for more details. -${ - definedHooks.length > 0 - ? `Hooks that are defined and available for tasks are: ${definedHooks.map(s.hook).join(', ')}` - : `There are no hooks defined by this app's plugins. You probably need to install some plugins to define hooks.` -}. ` +const formatPlugin = (plugin: Plugin): string => + plugin.id === 'app root' ? s.app('your app') : `plugin ${s.plugin(plugin.id)}` + export type InvalidOption = [string, z.ZodError] -export const formatInvalidOptions = ( +export const formatInvalidOption = ([id, error]: InvalidOption): string => + fromZodError(error, { prefix: `- ${id} has the issue(s)` }).message + +export const formatInvalidPluginOptions = ( invalidOptions: InvalidOption[] ): string => `Options are defined in your Tool Kit configuration that are the wrong types: -${invalidOptions - .map(([plugin, error]) => fromZodError(error, { prefix: `- ${s.plugin(plugin)} has the issue(s)` }).message) - .join('\n')} +${invalidOptions.map(([plugin, error]) => formatInvalidOption([s.plugin(plugin), error])).join('\n')} Please update the options so that they are the expected types. You can refer to the README for the plugin for examples and descriptions of the options used. ` -export const formatUnusedOptions = ( +export const formatUnusedPluginOptions = ( unusedOptions: string[], definedPlugins: string[] ): string => `Options are defined in your Tool Kit configuration for plugins that don't exist: @@ -121,8 +125,24 @@ ${ }. ` +export const formatUnusedTaskOptions = ( + unusedOptions: string[], + definedTasks: string[] +): string => `Options are defined in your Tool Kit configuration for tasks that don't exist: + +${unusedOptions.map((optionName) => `- ${s.task(optionName)}`).join('\n')} + +They could be misspelt, or defined by a Tool Kit plugin that isn't installed in this app. + +${ + definedTasks.length > 0 + ? `Task that are defined and can have options set are: ${definedTasks.map(s.task).join(', ')}` + : `You don't have currently any plugins installed that provide tasks. You'll need to install some plugins before options can be set.` +}. +` + export const formatUninstalledHooks = ( - uninstalledHooks: Hook[] + uninstalledHooks: Hook[] ): string => `These hooks aren't installed into your app: ${uninstalledHooks.map((hook) => `- ${s.hook(hook.id || 'unknown event')}`).join('\n')} @@ -130,11 +150,11 @@ ${uninstalledHooks.map((hook) => `- ${s.hook(hook.id || 'unknown event')}`).join Run ${s.task('dotcom-tool-kit --install')} to install these hooks. ` -type Missing = { hook: HookTask; tasks: string[] } +type Missing = { command: CommandTask; tasks: OptionsForTask[] } const formatMissingTask = (missing: Missing): string => - `- ${missing.tasks.map(s.task).join(', ')} ${s.dim( - `(assigned to ${s.hook(missing.hook.id)} by ${formatPlugin(missing.hook.plugin)})` + `- ${missing.tasks.map((task) => s.task(task.task)).join(', ')} ${s.dim( + `(assigned to ${s.hook(missing.command.id)} by ${formatPlugin(missing.command.plugin)})` )}` export const formatMissingTasks = ( @@ -165,3 +185,5 @@ export function formatPluginTree(plugin: Plugin): string[] { ) ] } + +export const indentReasons = (reasons: string): string => reasons.replace(/\n/g, '\n ') diff --git a/core/cli/src/plugin.ts b/core/cli/src/plugin.ts index b7829f51b..57e7882a6 100644 --- a/core/cli/src/plugin.ts +++ b/core/cli/src/plugin.ts @@ -1,104 +1,26 @@ import { styles as s } from '@dotcom-tool-kit/logger' -import { - Hook, - joinValidated, - mapValidated, - mapValidationError, - Plugin, - PluginModule, - reduceValidated, - Task, - Valid, - Validated -} from '@dotcom-tool-kit/types' -import isPlainObject from 'lodash/isPlainObject' -import resolveFrom from 'resolve-from' +import { CURRENT_RC_FILE_VERSION, type Plugin } from '@dotcom-tool-kit/plugin' +import type { RawConfig, ValidPluginsConfig } from '@dotcom-tool-kit/config' +import { invalid, reduceValidated, valid, Validated } from '@dotcom-tool-kit/validated' +import * as path from 'path' import type { Logger } from 'winston' -import { PluginOptions, RawConfig, ValidPluginsConfig } from './config' -import { Conflict, isConflict } from './conflict' -import type { HookTask } from './hook' import { loadToolKitRC } from './rc-file' - -type RawPluginModule = Partial - -function isDescendent(possibleAncestor: Plugin, possibleDescendent: Plugin): boolean { - if (!possibleDescendent.parent) { - return false - } else if (possibleDescendent.parent === possibleAncestor) { - return true - } else { - return isDescendent(possibleAncestor, possibleDescendent.parent) - } -} - -const indentReasons = (reasons: string): string => reasons.replace(/\n/g, '\n ') - -export function validatePlugin(plugin: unknown): Validated { - const errors: string[] = [] - const rawPlugin = plugin as RawPluginModule - - if (rawPlugin.tasks) { - if (!Array.isArray(rawPlugin.tasks)) { - errors.push(`the exported ${s.code('tasks')} value from this plugin is not an array`) - } else { - const validatedTasks = reduceValidated( - rawPlugin.tasks.map((task) => - mapValidationError(Task.isCompatible(task), (reasons) => [ - `the task ${s.task(task.name)} is not a compatible instance of ${s.code( - 'Task' - )}:\n - ${reasons.join('\n - ')}` - ]) - ) - ) - if (!validatedTasks.valid) { - errors.push(...validatedTasks.reasons) - } - } - } - - if (rawPlugin.hooks) { - if (!isPlainObject(rawPlugin.hooks)) { - errors.push(`the exported ${s.code('hooks')} value from this plugin is not an object`) - } else { - const validatedHooks = reduceValidated( - Object.entries(rawPlugin.hooks).map(([id, hook]) => - mapValidationError(Hook.isCompatible(hook), (reasons) => [ - `the hook ${s.hook(id)} is not a compatible instance of ${s.code('Hook')}:\n - ${reasons.join( - '\n - ' - )}` - ]) - ) - ) - if (!validatedHooks.valid) { - errors.push(...validatedHooks.reasons) - } - } - } - - if (errors.length > 0) { - return { valid: false, reasons: errors } - } else { - const pluginModule = { tasks: rawPlugin.tasks ?? [], hooks: rawPlugin.hooks ?? {} } - return { valid: true, value: pluginModule } - } -} - -async function importPlugin(pluginPath: string): Promise> { - try { - // pluginPath is an absolute resolved path to a plugin module as found from its parent - const pluginModule = (await import(pluginPath)) as unknown - return validatePlugin(pluginModule) - } catch (e) { - const err = e as Error - return { - valid: false, - reasons: [ - `an error was thrown when loading this plugin's entrypoint:\n ${s.code( - indentReasons(err.toString()) - )}` - ] - } - } +import { indentReasons } from './messages' +import { mergeTasks } from './plugin/merge-tasks' +import { mergeHooks } from './plugin/merge-hooks' +import { mergeCommands } from './plugin/merge-commands' +import { mergePluginOptions } from './plugin/merge-plugin-options' +import { mergeInits } from './plugin/merge-inits' +import { mergeTaskOptions } from './plugin/merge-task-options' + +function resolveRoot(id: string, root: string): string { + const isPath = id.startsWith('.') || id.startsWith('/') + // resolve the package.json of a plugin as many plugins don't have valid + // entrypoints now that we're intending their tasks/hooks to be loaded via + // entrypoints defined in config + const modulePath = isPath ? id : path.join(id, 'package.json') + const resolvedPath = require.resolve(modulePath, { paths: [root] }) + return path.dirname(resolvedPath) } export async function loadPlugin( @@ -116,195 +38,87 @@ export async function loadPlugin( // load plugin relative to the parent plugin const root = parent ? parent.root : process.cwd() - let pluginRoot: string - try { - pluginRoot = isAppRoot ? root : resolveFrom(root, id) - } catch (e) { - return { valid: false, reasons: [`could not find path for name ${s.filepath(id)}`] } + const pluginRoot = isAppRoot ? root : resolveRoot(id, root) + if (!pluginRoot) { + return invalid([`could not find path for name ${s.filepath(id)}`]) } - const plugin: Valid = { - valid: true, - value: { - id, - root: pluginRoot, - parent - } + const plugin = { + id, + root: pluginRoot, + parent, + rcFile: await loadToolKitRC(logger, pluginRoot), + children: [] as Plugin[] } - config.plugins[id] = plugin - - // start loading rc file in the background - const rcFilePromise = loadToolKitRC(logger, pluginRoot, isAppRoot) - - // start loading module in the background - const pluginModulePromise: Promise> = isAppRoot - ? Promise.resolve({ valid: true, value: undefined }) - : importPlugin(pluginRoot) + if (!isAppRoot && plugin.rcFile.version !== CURRENT_RC_FILE_VERSION) { + return invalid([ + `plugin ${s.plugin(id)} has a v${s.code((plugin.rcFile.version ?? 1).toString())} ${s.code( + '.toolkitrc.yml' + )}, but this version of Tool Kit can only load v${s.code( + CURRENT_RC_FILE_VERSION.toString() + )}. please update this plugin.` + ]) + } - // ESlint disable explanation: erroring due to a possible race condition but is a false positive since the plugin variable isn't from another scope and can't be written to concurrently. + // ESlint disable explanation: erroring due to a possible race condition but is a false positive since the config variable isn't from another scope and can't be written to concurrently. // eslint-disable-next-line require-atomic-updates - plugin.value.rcFile = await rcFilePromise + config.plugins[id] = valid(plugin) - // start loading child plugins in the background - const childrenPromise = Promise.all( - plugin.value.rcFile.plugins.map((child) => loadPlugin(child, config, logger, plugin.value)) + const children = await Promise.all( + plugin.rcFile.plugins.map((child) => loadPlugin(child, config, logger, plugin)) ) - // wait for pending promises concurrently - const [module, children] = await Promise.all([pluginModulePromise, childrenPromise]) - - const validatedModule = mapValidationError(module, (reasons) => [ - indentReasons(`plugin ${s.plugin(id)} failed to load because:\n- ${reasons.join('\n- ')}`) - ]) - const validatedChildren = mapValidationError(reduceValidated(children), (reasons) => [ - indentReasons(`some child plugins of ${s.plugin(id)} failed to load:\n- ${reasons.join('\n- ')}`) - ]) - - return mapValidated(joinValidated(validatedModule, validatedChildren), ([module, children]) => { - // avoid cloning the plugin value with an object spread as we do object - // reference comparisons in multiple places - plugin.value.module = module - plugin.value.children = children - return plugin.value - }) + return reduceValidated(children) + .mapError((reasons) => [ + indentReasons(`some child plugins of ${s.plugin(id)} failed to load:\n- ${reasons.join('\n- ')}`) + ]) + .map((children) => { + // avoid cloning the plugin value with an object spread as we do object + // reference comparisons in multiple places + plugin.children = children + return plugin + }) } -export function resolvePlugin(plugin: Plugin, config: ValidPluginsConfig, logger: Logger): void { +export function resolvePluginOptions(plugin: Plugin, config: ValidPluginsConfig): void { // don't resolve plugins that have already been resolved to prevent self-conflicts // between plugins included at multiple points in the tree - if (config.resolvedPlugins.has(plugin)) { + if (config.resolutionTrackers.resolvedPluginOptions.has(plugin.id)) { return } if (plugin.children) { // resolve child plugins first so parents can override the things their children set for (const child of plugin.children) { - resolvePlugin(child, config, logger) + resolvePluginOptions(child, config) } } - if (plugin.module) { - // add plugin tasks to our task registry, handling any conflicts - for (const newTask of plugin.module.tasks || []) { - const taskId = newTask.name - const existingTask = config.tasks[taskId] - - newTask.plugin = plugin - newTask.id = taskId + mergePluginOptions(config, plugin) - if (existingTask) { - const conflicting = isConflict(existingTask) ? existingTask.conflicting : [existingTask] - - config.tasks[taskId] = { - plugin, - conflicting: conflicting.concat(newTask) - } - } else { - config.tasks[taskId] = newTask - } - } - - // add hooks to the registry, handling any conflicts - // TODO refactor with command conflict handler - for (const [hookId, hookClass] of Object.entries(plugin.module.hooks || [])) { - const existingHook = config.hooks[hookId] - const newHook = new hookClass(logger) - - newHook.id = hookId - newHook.plugin = plugin - - if (existingHook) { - const conflicting = isConflict(existingHook) ? existingHook.conflicting : [existingHook] + config.resolutionTrackers.resolvedPluginOptions.add(plugin.id) +} - config.hooks[hookId] = { - plugin, - conflicting: conflicting.concat(newHook) - } - } else { - config.hooks[hookId] = newHook - } - } +export function resolvePlugin(plugin: Plugin, config: ValidPluginsConfig, logger: Logger): void { + // don't resolve plugins that have already been resolved to prevent self-conflicts + // between plugins included at multiple points in the tree + if (config.resolutionTrackers.resolvedPlugins.has(plugin.id)) { + return } - if (plugin.rcFile) { - // load plugin hook tasks. do this after loading child plugins, so - // parent hooks get assigned after child hooks and can override them - for (const [id, configHookTask] of Object.entries(plugin.rcFile.hooks)) { - // handle conflicts between hooks from different plugins - const existingHookTask = config.hookTasks[id] - const newHookTask: HookTask = { - id, - plugin, - tasks: Array.isArray(configHookTask) ? configHookTask : [configHookTask] - } - - if (existingHookTask) { - const existingFromDescendent = isDescendent(plugin, existingHookTask.plugin) - - // plugins can only override hook tasks from their descendents, otherwise that's a conflict - // return a conflict either listing this hook and the siblings, - // or merging in a previously-generated hook - if (!existingFromDescendent) { - const conflicting = isConflict(existingHookTask) ? existingHookTask.conflicting : [existingHookTask] - - const conflict: Conflict = { - plugin, - conflicting: conflicting.concat(newHookTask) - } - - config.hookTasks[id] = conflict - } else { - // if we're here, any existing hook is from a child plugin, - // so the parent always overrides it - config.hookTasks[id] = newHookTask - } - } else { - // this hook task might not have been set yet, in which case use the new one - config.hookTasks[id] = newHookTask - } - } - - // merge options from this plugin's config with any options we've collected already - // TODO this is almost the exact same code as for hooks, refactor - for (const [id, configOptions] of Object.entries(plugin.rcFile.options)) { - // users can specify root options with the dotcom-tool-kit key to mirror - // the name of the root npm package - const pluginId = id === 'dotcom-tool-kit' ? 'app root' : id - const existingOptions = config.options[pluginId] - - const pluginOptions: PluginOptions = { - options: configOptions, - plugin, - forPlugin: config.plugins[pluginId] - } - - if (existingOptions) { - const existingFromDescendent = isDescendent(plugin, existingOptions.plugin) - - // plugins can only override options from their descendents, otherwise it's a conflict - // return a conflict either listing these options and the sibling's, - // or merging in previously-generated options - if (!existingFromDescendent) { - const conflicting = isConflict(existingOptions) ? existingOptions.conflicting : [existingOptions] - - const conflict: Conflict = { - plugin, - conflicting: conflicting.concat(pluginOptions) - } - - config.options[pluginId] = conflict - } else { - // if we're here, any existing options are from a child plugin, - // so merge in overrides from the parent - config.options[pluginId] = { ...existingOptions, ...pluginOptions } - } - } else { - // this options key might not have been set yet, in which case use the new one - config.options[pluginId] = pluginOptions - } + if (plugin.children) { + // resolve child plugins first so parents can override the things their children set + for (const child of plugin.children) { + resolvePlugin(child, config, logger) } } - config.resolvedPlugins.add(plugin) + mergeTasks(config, plugin) + mergeHooks(config, plugin) + mergeCommands(config, plugin, logger) + mergeTaskOptions(config, plugin) + mergeInits(config, plugin) + + config.resolutionTrackers.resolvedPlugins.add(plugin.id) } diff --git a/core/cli/src/plugin/entry-point.ts b/core/cli/src/plugin/entry-point.ts new file mode 100644 index 000000000..e1b1ecad0 --- /dev/null +++ b/core/cli/src/plugin/entry-point.ts @@ -0,0 +1,60 @@ +import { styles as s } from '@dotcom-tool-kit/logger' + +import type { Base } from '@dotcom-tool-kit/base' +import type { EntryPoint } from '@dotcom-tool-kit/plugin' +import { Validated, invalid } from '@dotcom-tool-kit/validated' +import { isPlainObject } from 'lodash' +import { indentReasons } from '../messages' + +const isPlainObjectGuard = (value: unknown): value is Record => isPlainObject(value) + +// the subclasses of Base have different constructor signatures so we need to omit +// the constructor from the type bound here so you can actually pass in a subclass +export async function importEntryPoint>( + type: T, + entryPoint: EntryPoint +): Promise> { + const resolvedPath = require.resolve(entryPoint.modulePath, { paths: [entryPoint.plugin.root] }) + + if (!resolvedPath) { + return invalid([ + `could not find entrypoint ${s.filepath(entryPoint.modulePath)} in plugin ${s.plugin( + entryPoint.plugin.id + )}` + ]) + } + + let pluginModule: unknown + try { + pluginModule = await import(resolvedPath) + } catch (e) { + const err = e as Error + return invalid([ + `an error was thrown when loading entrypoint ${s.filepath(entryPoint.modulePath)} in plugin ${s.plugin( + entryPoint.plugin.id + )}:\n ${s.code(indentReasons(err.toString()))}` + ]) + } + + if ( + isPlainObjectGuard(pluginModule) && + 'default' in pluginModule && + typeof pluginModule.default === 'function' + ) { + const name = pluginModule.default.name + + return type + .isCompatible(pluginModule.default) + .mapError((reasons) => [ + `the ${type.name.toLowerCase()} ${s.hook(name)} is not a compatible instance of ${s.code( + type.name + )}:\n - ${reasons.join('\n - ')}` + ]) + } else { + return invalid([ + `entrypoint ${s.filepath(entryPoint.modulePath)} in plugin ${s.plugin( + entryPoint.plugin.id + )} does not have a ${s.code('default')} export` + ]) + } +} diff --git a/core/cli/src/plugin/is-descendent.ts b/core/cli/src/plugin/is-descendent.ts new file mode 100644 index 000000000..798746648 --- /dev/null +++ b/core/cli/src/plugin/is-descendent.ts @@ -0,0 +1,11 @@ +import type { Plugin } from '@dotcom-tool-kit/plugin' + +export function isDescendent(possibleAncestor: Plugin, possibleDescendent: Plugin): boolean { + if (!possibleDescendent.parent) { + return false + } else if (possibleDescendent.parent === possibleAncestor) { + return true + } else { + return isDescendent(possibleAncestor, possibleDescendent.parent) + } +} diff --git a/core/cli/src/plugin/merge-commands.ts b/core/cli/src/plugin/merge-commands.ts new file mode 100644 index 000000000..b16c0f89a --- /dev/null +++ b/core/cli/src/plugin/merge-commands.ts @@ -0,0 +1,69 @@ +import type { CommandTask, Plugin } from '@dotcom-tool-kit/plugin' +import type { ValidPluginsConfig } from '@dotcom-tool-kit/config' +import { Conflict, isConflict } from '@dotcom-tool-kit/conflict' +import { isDescendent } from './is-descendent' +import { Logger } from 'winston' +import { styles as s } from '@dotcom-tool-kit/logger' +import path from 'path' + +export const mergeCommands = (config: ValidPluginsConfig, plugin: Plugin, logger: Logger) => { + if (plugin.rcFile) { + let commands = plugin.rcFile.commands + + // TODO:KB:20240410 remove this legacy hooks field handling and the associated + // field in the type definitions in a future major version + if (plugin.rcFile.hooks) { + commands = plugin.rcFile.hooks + logger.warn( + `${s.code('hooks')} is deprecated in ${s.filepath('.toolkitrc.yml')}. please rename ${s.code( + 'hooks' + )} to ${s.code('commands')} in ${s.filepath(path.join(plugin.root, '.toolkitrc.yml'))}.` + ) + } + + for (const [id, configCommandTask] of Object.entries(commands)) { + // handle conflicts between commands from different plugins + const existingCommandTask = config.commandTasks[id] + const newCommandTask: CommandTask = { + id, + plugin, + tasks: (Array.isArray(configCommandTask) ? configCommandTask : [configCommandTask]).flatMap( + (commandTask) => { + if (typeof commandTask === 'string') { + return [{ task: commandTask, options: {}, plugin }] + } + + return Object.entries(commandTask).map(([task, options]) => ({ task, options, plugin })) + } + ) + } + + if (existingCommandTask) { + const existingFromDescendent = isDescendent(plugin, existingCommandTask.plugin) + + // plugins can only override command tasks from their descendents, otherwise that's a conflict + // return a conflict either listing this command and the siblings, + // or merging in a previously-generated command + if (!existingFromDescendent) { + const conflicting = isConflict(existingCommandTask) + ? existingCommandTask.conflicting + : [existingCommandTask] + + const conflict: Conflict = { + plugin, + conflicting: conflicting.concat(newCommandTask) + } + + config.commandTasks[id] = conflict + } else { + // if we're here, any existing command is from a child plugin, + // so the parent always overrides it + config.commandTasks[id] = newCommandTask + } + } else { + // this command task might not have been set yet, in which case use the new one + config.commandTasks[id] = newCommandTask + } + } + } +} diff --git a/core/cli/src/plugin/merge-hooks.ts b/core/cli/src/plugin/merge-hooks.ts new file mode 100644 index 000000000..7579ef703 --- /dev/null +++ b/core/cli/src/plugin/merge-hooks.ts @@ -0,0 +1,29 @@ +import type { EntryPoint, Plugin } from '@dotcom-tool-kit/plugin' +import type { ValidPluginsConfig } from '@dotcom-tool-kit/config' +import { isConflict } from '@dotcom-tool-kit/conflict' + +export const mergeHooks = (config: ValidPluginsConfig, plugin: Plugin) => { + if (plugin.rcFile) { + // add hooks to the registry, handling any conflicts + // TODO refactor with command conflict handler + for (const [hookName, hookSpec] of Object.entries(plugin.rcFile.installs || {})) { + const existingHookId = config.hooks[hookName] + const entryPoint: EntryPoint = { + plugin, + modulePath: hookSpec.entryPoint + } + + if (existingHookId) { + const conflicting = isConflict(existingHookId) ? existingHookId.conflicting : [existingHookId] + + config.hooks[hookName] = { + plugin, + conflicting: conflicting.concat(entryPoint) + } + } else { + config.hooks[hookName] = entryPoint + hookSpec.managesFiles?.forEach((file) => config.hookManagedFiles.add(file)) + } + } + } +} diff --git a/core/cli/src/plugin/merge-inits.ts b/core/cli/src/plugin/merge-inits.ts new file mode 100644 index 000000000..89a5c2372 --- /dev/null +++ b/core/cli/src/plugin/merge-inits.ts @@ -0,0 +1,14 @@ +import type { Plugin } from '@dotcom-tool-kit/plugin' +import type { ValidPluginsConfig } from '@dotcom-tool-kit/config' + +export const mergeInits = (config: ValidPluginsConfig, plugin: Plugin) => { + if (plugin.rcFile) { + // no conflict resolution needed; we'll just run them all ig + config.inits.push( + ...plugin.rcFile.init.map((init) => ({ + plugin, + modulePath: init + })) + ) + } +} diff --git a/core/cli/src/plugin/merge-plugin-options.ts b/core/cli/src/plugin/merge-plugin-options.ts new file mode 100644 index 000000000..7dba580fe --- /dev/null +++ b/core/cli/src/plugin/merge-plugin-options.ts @@ -0,0 +1,48 @@ +import type { Plugin, OptionsForPlugin } from '@dotcom-tool-kit/plugin' +import type { ValidPluginsConfig } from '@dotcom-tool-kit/config' +import { isDescendent } from './is-descendent' +import { Conflict, isConflict } from '@dotcom-tool-kit/conflict' + +// merge options from this plugin's config with any options we've collected already +// TODO this is almost the exact same code as for command tasks, refactor +export const mergePluginOptions = (config: ValidPluginsConfig, plugin: Plugin) => { + if (plugin.rcFile) { + for (const [id, configOptions] of Object.entries(plugin.rcFile.options.plugins)) { + // users can specify root options with the dotcom-tool-kit key to mirror + // the name of the root npm package + const pluginId = id === 'dotcom-tool-kit' ? 'app root' : id + const existingOptions = config.pluginOptions[pluginId] + + const pluginOptions: OptionsForPlugin = { + options: configOptions, + plugin, + forPlugin: config.plugins[pluginId] + } + + if (existingOptions) { + const existingFromDescendent = isDescendent(plugin, existingOptions.plugin) + + // plugins can only override options from their descendents, otherwise it's a conflict + // return a conflict either listing these options and the sibling's, + // or merging in previously-generated options + if (!existingFromDescendent) { + const conflicting = isConflict(existingOptions) ? existingOptions.conflicting : [existingOptions] + + const conflict: Conflict = { + plugin, + conflicting: conflicting.concat(pluginOptions) + } + + config.pluginOptions[pluginId] = conflict + } else { + // if we're here, any existing options are from a child plugin, + // so merge in overrides from the parent + config.pluginOptions[pluginId] = { ...existingOptions, ...pluginOptions } + } + } else { + // this options key might not have been set yet, in which case use the new one + config.pluginOptions[pluginId] = pluginOptions + } + } + } +} diff --git a/core/cli/src/plugin/merge-task-options.ts b/core/cli/src/plugin/merge-task-options.ts new file mode 100644 index 000000000..d4ebd392f --- /dev/null +++ b/core/cli/src/plugin/merge-task-options.ts @@ -0,0 +1,46 @@ +import type { Plugin, OptionsForTask } from '@dotcom-tool-kit/plugin' +import type { ValidPluginsConfig } from '@dotcom-tool-kit/config' +import { type Conflict, isConflict } from '@dotcom-tool-kit/conflict' + +import { isDescendent } from './is-descendent' + +// merge options from this plugin's config with any options we've collected already +// TODO this is almost the exact same code as for command tasks, refactor +export const mergeTaskOptions = (config: ValidPluginsConfig, plugin: Plugin) => { + if (plugin.rcFile) { + for (const [taskId, configOptions] of Object.entries(plugin.rcFile.options.tasks)) { + const existingOptions = config.taskOptions[taskId] + + const taskOptions: OptionsForTask = { + options: configOptions, + plugin, + task: taskId + } + + if (existingOptions) { + const existingFromDescendent = isDescendent(plugin, existingOptions.plugin) + + // plugins can only override options from their descendents, otherwise it's a conflict + // return a conflict either listing these options and the sibling's, + // or merging in previously-generated options + if (!existingFromDescendent) { + const conflicting = isConflict(existingOptions) ? existingOptions.conflicting : [existingOptions] + + const conflict: Conflict = { + plugin, + conflicting: conflicting.concat(taskOptions) + } + + config.taskOptions[taskId] = conflict + } else { + // if we're here, any existing options are from a child plugin, + // so merge in overrides from the parent + config.taskOptions[taskId] = { ...existingOptions, ...taskOptions } + } + } else { + // this options key might not have been set yet, in which case use the new one + config.taskOptions[taskId] = taskOptions + } + } + } +} diff --git a/core/cli/src/plugin/merge-tasks.ts b/core/cli/src/plugin/merge-tasks.ts new file mode 100644 index 000000000..cfecacbea --- /dev/null +++ b/core/cli/src/plugin/merge-tasks.ts @@ -0,0 +1,27 @@ +import type { EntryPoint, Plugin } from '@dotcom-tool-kit/plugin' +import type { ValidPluginsConfig } from '@dotcom-tool-kit/config' +import { isConflict } from '@dotcom-tool-kit/conflict' + +// add plugin tasks to our task registry, handling any conflicts +export const mergeTasks = (config: ValidPluginsConfig, plugin: Plugin) => { + if (plugin.rcFile) { + for (const [taskName, modulePath] of Object.entries(plugin.rcFile.tasks || {})) { + const existingTaskId = config.tasks[taskName] + const entryPoint: EntryPoint = { + plugin, + modulePath + } + + if (existingTaskId) { + const conflicting = isConflict(existingTaskId) ? existingTaskId.conflicting : [existingTaskId] + + config.tasks[taskName] = { + plugin, + conflicting: conflicting.concat(entryPoint) + } + } else { + config.tasks[taskName] = entryPoint + } + } + } +} diff --git a/core/cli/src/plugin/options.ts b/core/cli/src/plugin/options.ts new file mode 100644 index 000000000..6696ea5f4 --- /dev/null +++ b/core/cli/src/plugin/options.ts @@ -0,0 +1,180 @@ +import { ValidPluginsConfig } from '@dotcom-tool-kit/config' +import { isConflict } from '@dotcom-tool-kit/conflict' +import { OptionsForPlugin, RCFile, type Plugin } from '@dotcom-tool-kit/plugin' +import { type PluginOptions, PluginSchemas, legacyPluginOptions } from '@dotcom-tool-kit/schemas' +import { invalid, reduceValidated, valid, Validated } from '@dotcom-tool-kit/validated' + +import type { Logger } from 'winston' +import { ZodError, ZodIssueCode } from 'zod' +import { styles } from '@dotcom-tool-kit/logger' + +import { toolKitIfDefinedIdent, toolKitOptionIdent } from '../rc-file' +import { InvalidOption } from '../messages' + +export const validatePluginOptions = (logger: Logger, config: ValidPluginsConfig): InvalidOption[] => { + const invalidOptions: InvalidOption[] = [] + + for (const [id, plugin] of Object.entries(config.plugins)) { + const pluginId = id as keyof PluginOptions + const pluginOptions = config.pluginOptions[pluginId] + if (pluginOptions && isConflict(pluginOptions)) { + continue + } + + const pluginSchema = PluginSchemas[pluginId] + + if (!pluginSchema) { + logger.silly(`skipping validation of ${pluginId} plugin as no schema can be found`) + + // TODO:KB:20240412 remove legacyPluginOptions in a future major version + if (pluginOptions && pluginId in legacyPluginOptions) { + const movedToTask = legacyPluginOptions[pluginId] + invalidOptions.push([ + id, + new ZodError([ + { + message: `options for ${styles.plugin(id)} have moved to the ${styles.task(movedToTask)} task`, + code: ZodIssueCode.custom, + path: [] + } + ]) + ]) + } + + continue + } + + const result = pluginSchema.safeParse(pluginOptions?.options ?? {}) + if (result.success) { + // Set up options entry for plugins that don't have options specified + // explicitly. They could still have default options that are set by zod. + if (!pluginOptions) { + config.pluginOptions[pluginId] = { + options: result.data, + plugin: config.plugins['app root'], + forPlugin: plugin + } + } else { + pluginOptions.options = result.data + } + } else { + invalidOptions.push([id, result.error]) + } + } + + return invalidOptions +} + +export const substituteOptionTags = (plugin: Plugin, config: ValidPluginsConfig): void => { + // foo.bar gets the 'bar' option set for the 'foo' plugin + const resolveOptionPath = (optionPath: string): unknown => { + const [pluginName, optionName] = optionPath.split('.', 2) + return (config.pluginOptions[pluginName] as OptionsForPlugin)?.options[optionName] + } + + // throw an error if there are tags in plugin option fields to avoid circular + // references + const validateTagPath = (path: (string | number)[]): string | void => { + if (path[0] === 'options' && path[1] === 'plugins') { + return `YAML tag referencing options used at path '${path.join('.')}'` + } + } + + // recursively walk through the parsed config, searching for the tag + // identifiers we've inserted during parsing, and substitute them for + // resolved option values + const deeplySubstitute = (node: unknown, path: (string | number)[]): Validated => { + if (Array.isArray(node)) { + return reduceValidated(node.map((item, i) => deeplySubstitute(item, [...path, i]))) + } else if (node && typeof node === 'object') { + const entries = Object.entries(node) + const substituted: Validated<[string, unknown]>[] = [] + for (const entry of entries) { + const subbedEntry = reduceValidated( + // allow both keys and (string) values to be substituted by options + entry.map((val) => { + if (typeof val === 'string' && val.startsWith(toolKitOptionIdent)) { + // check the tag path each time so that we can have a separate + // error for each incorrect use of the tag + const validationError = validateTagPath([...path, entry[0]]) + if (validationError) { + return invalid([validationError]) + } else { + // the option path is concatenated after the !toolkit/option + // identifier + const optionPath = val.slice(toolKitOptionIdent.length) + const resolvedOption = resolveOptionPath(optionPath) + if (typeof resolvedOption === 'string') { + return valid(resolvedOption) + } else { + return invalid([ + `Option '${optionPath}' referenced at path '${path.join( + '.' + )}' does not resolve to a string (resolved to ${resolvedOption})` + ]) + } + } + } else { + return valid(val) + } + }) + ) + if (!subbedEntry.valid) { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any -- + * Invalid objects don't need to match the inner type + **/ + substituted.push(subbedEntry as Validated) + continue + } + + const [key, value] = subbedEntry.value + if (key.startsWith(toolKitIfDefinedIdent)) { + const validationError = validateTagPath(path) + if (validationError) { + substituted.push(invalid([validationError])) + } + // the option path is concatenated after the !toolkit/if-defined + // identifier + const optionPath = key.slice(toolKitIfDefinedIdent.length) + const optionValue = resolveOptionPath(optionPath) + // keep walking the path if we've found an error here so we can + // gather even more errors to show the user. else skip traversal if + // we aren't going to include the node + if (optionValue || validationError) { + const subbedValues = deeplySubstitute(value, path) + if (subbedValues.valid) { + substituted.push(...Object.entries(subbedValues.value as object).map((v) => valid(v))) + } else { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any -- + * Invalid objects don't need to match the inner type + **/ + substituted.push(subbedValues as Validated) + } + } + } else { + substituted.push(deeplySubstitute(value, [...path, key]).map((subbedValue) => [key, subbedValue])) + } + } + return reduceValidated(substituted).map(Object.fromEntries) + } else { + return valid(node) + } + } + + // avoid running substitution over a config repeatedly – all substitutions + // will have been made in the first pass + if (config.resolutionTrackers.substitutedPlugins.has(plugin.id)) { + return + } + if (plugin.children) { + for (const child of plugin.children) { + substituteOptionTags(child, config) + } + } + if (plugin.rcFile) { + plugin.rcFile = deeplySubstitute(plugin.rcFile, []).unwrap( + 'cannot reference plugin options when specifying options' + ) as RCFile + } + config.resolutionTrackers.substitutedPlugins.add(plugin.id) +} diff --git a/core/cli/src/plugin/reduce-installations.ts b/core/cli/src/plugin/reduce-installations.ts new file mode 100644 index 000000000..8719c6f58 --- /dev/null +++ b/core/cli/src/plugin/reduce-installations.ts @@ -0,0 +1,83 @@ +import type { Logger } from 'winston' +import type { HookClass, HookInstallation } from '@dotcom-tool-kit/base' +import type { Plugin } from '@dotcom-tool-kit/plugin' +import type { ValidConfig } from '@dotcom-tool-kit/config' +import { HookSchemas, HookOptions } from '@dotcom-tool-kit/schemas' +import { Conflict, isConflict } from '@dotcom-tool-kit/conflict' +import { groupBy } from 'lodash' + +const extractForHook = (installation: HookInstallation | Conflict): string => + isConflict(installation) ? installation.conflicting[0].forHook : installation.forHook + +// this function recursively collects all the hook installation requests from all plugins, +// and merges them into a single, flat array of HookInstallation objects and/or Conflicts. +// +// it works depth-first (i.e. recurses into child plugins first), and considers how to +// merge options or create conflicts in two stages: 1) when considering all installations +// from child plugins, and 2) when considering how a parent plugin would override its +// children. these steps are separate as a particular parent might not provide an override +// for all its children, and different hooks could expect different ways of resolving +// conflicts. +// +// the actual logic for this is delegated to static methods on Hook classes, +// `Hook.mergeChildInstallations` and `Hook.overrideChildInstallations`, so separate hooks +// can provide different logic for these steps. +// +// the default logic in the base Hook class is to always consider multiple installations +// from child plugins as a conflict, and always consider a installation in a parent as +// completely replacing any installations from children. +// +// for example, for a plugin `p` that depends on children `a`, `b`, and `c` that all provide +// configuration for the `PackageJson` hook, this function will: +// - do all this logic for `a`, `b`, and `c` +// - call `Hook.mergeChildInstallations` with the appropriate concrete Hook class, and +// the resulting installations and/or conflicts from `a`, `b`, and `c` +// - call `Hook.overrideChildInstallations` with the appropriate concrete Hook class, and + +// the resulting installations and/or conflicts from `Hook.mergeChildInstallations` and `p` +export async function reducePluginHookInstallations( + logger: Logger, + config: ValidConfig, + hookClasses: Record, + plugin: Plugin +): Promise<(HookInstallation | Conflict)[]> { + if (!plugin.rcFile || config.resolutionTrackers.reducedInstallationPlugins.has(plugin.id)) { + return [] + } + config.resolutionTrackers.reducedInstallationPlugins.add(plugin.id) + + const rawChildInstallations = await Promise.all( + (plugin.children ?? []).map((child) => reducePluginHookInstallations(logger, config, hookClasses, child)) + ).then((installations) => installations.flat()) + + const childInstallations = Object.entries(groupBy(rawChildInstallations, extractForHook)).flatMap( + ([forHook, installations]) => { + const hookClass = hookClasses[forHook] + + return hookClass.mergeChildInstallations(plugin, installations) + } + ) + + if (plugin.rcFile.options.hooks.length === 0) { + return childInstallations + } + + return plugin.rcFile.options.hooks.flatMap((hookEntry) => + Object.entries(hookEntry).flatMap(([id, configHookOptions]) => { + const hookClass = hookClasses[id] + const parsedOptions = HookSchemas[id as keyof HookOptions].parse(configHookOptions) + + const installation: HookInstallation = { + options: parsedOptions, + plugin, + forHook: id, + hookConstructor: hookClass + } + + const childInstallationsForHook = childInstallations.filter( + (childInstallation) => id === extractForHook(childInstallation) + ) + return hookClass.overrideChildInstallations(plugin, installation, childInstallationsForHook) + }) + ) +} diff --git a/core/cli/src/postInstall.ts b/core/cli/src/postInstall.ts deleted file mode 100644 index 8777647d6..000000000 --- a/core/cli/src/postInstall.ts +++ /dev/null @@ -1,53 +0,0 @@ -import path from 'path' -import { promises as fs } from 'fs' -import { semVerRegex } from '@dotcom-tool-kit/types/lib/npm' -import * as YAML from 'yaml' -import { Pair, YAMLMap, YAMLSeq } from 'yaml/types' -import merge from 'lodash/merge' -import type { Logger } from 'winston' -import { automatedComment, JobConfig } from '@dotcom-tool-kit/types/lib/circleci' - -/** - * This step adds the tags only filter to rest of the jobs in the workflow if there is a job that contains the semverRegex. - * CircleCI will only run the jobs if the rest of the jobs have the tags filter. - */ -export async function postInstall(logger: Logger): Promise { - const circleConfigPath = path.resolve(process.cwd(), '.circleci/config.yml') - try { - const rawCircleConfig = await fs.readFile(circleConfigPath, 'utf8') - if ( - rawCircleConfig && - rawCircleConfig.includes(semVerRegex.source) && - rawCircleConfig.startsWith(automatedComment) - ) { - logger.verbose('running postInstall step') - const yml = YAML.parseDocument(rawCircleConfig) - const workflows: YAMLMap = yml.get('workflows') - const toolkitWorkflow = workflows.get('tool-kit') - const jobs: YAMLSeq = toolkitWorkflow.get('jobs') - jobs?.items.forEach((jobItem, index) => { - const tagsFilterConfig: JobConfig = { filters: { tags: { only: `${semVerRegex}` } } } - if (jobItem.type === 'PLAIN') { - // eg. - checkout - const node = YAML.createNode({ [jobItem.value]: tagsFilterConfig }) - jobs.items[index] = node - } else { - const job: Pair = (jobItem as YAMLMap).items[0] - const existingFilter: Pair | undefined = job.value.items.filter( - (item: Pair) => item.key.value === 'filters' - )[0] - const merged = existingFilter ? merge(existingFilter.toJSON(), tagsFilterConfig) : tagsFilterConfig - const node = YAML.createNode(merged['filters']) - job.value.set('filters', node) - } - }) - logger.info(`writing postInstall results to file ${circleConfigPath}`) - await fs.writeFile(circleConfigPath, yml.toString()) - } - } catch (error) { - // Not an error if config file doesn't exist - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { - throw error - } - } -} diff --git a/core/cli/src/rc-file.ts b/core/cli/src/rc-file.ts index 6294b85c1..0b544158f 100644 --- a/core/cli/src/rc-file.ts +++ b/core/cli/src/rc-file.ts @@ -1,41 +1,116 @@ +import fs from 'node:fs/promises' import { styles as s } from '@dotcom-tool-kit/logger' -import { RCFile } from '@dotcom-tool-kit/types/src' -import { cosmiconfig } from 'cosmiconfig' +import type { RCFile } from '@dotcom-tool-kit/plugin' import * as path from 'path' import type { Logger } from 'winston' +import * as YAML from 'yaml' -export const explorer = cosmiconfig('toolkit', { ignoreEmptySearchPlaces: false }) -const emptyConfig = { plugins: [], hooks: {}, options: {} } -let rootConfig: string | undefined +const emptyConfig = { + plugins: [], + installs: {}, + tasks: {}, + commands: {}, + options: { plugins: {}, tasks: {}, hooks: [] }, + init: [] +} satisfies RCFile +// TODO:IM:20240418 define another type that accounts for the custom tags +// existing deeply within the file type RawRCFile = { - [key in keyof RCFile]?: RCFile[key] | null + [key in Exclude]?: RCFile[key] | null +} & { + options: + | { + [key in keyof RCFile['options']]?: RCFile['options'][key] | null + } + | null } -export async function loadToolKitRC(logger: Logger, root: string, isAppRoot: boolean): Promise { - const result = await explorer.search(root) +// yaml will automatically stringify any symbols in keys so just use strings +// that won't be used normally +export const toolKitOptionIdent = '__toolkit/option__' +export const toolKitIfDefinedIdent = '__toolkit/if-defined__' - if (!result?.config) { - return emptyConfig +// minimally define the two custom tags' identify callback so that yaml will +// parse them without warning but will never be use them when stringifying +const toolKitOption = { + identify: () => false, + tag: '!toolkit/option', + // wrap option path with identifier so we can substitute the option's value + // once it's been resolved later + resolve: (option) => `${toolKitOptionIdent}${option}` +} satisfies YAML.ScalarTag +const toolKitIfDefined = { + identify: () => false, + tag: '!toolkit/if-defined', + // the resolve callback doesn't allow us to manipulate the whole YAML.Pair + // with this tagged key, so just return it unchanged now and find the tag in + // a YAML.visit later + resolve: (value) => value +} satisfies YAML.ScalarTag + +export async function loadToolKitRC(logger: Logger, root: string): Promise { + let rawConfig: string + try { + rawConfig = await fs.readFile(path.join(root, '.toolkitrc.yml'), 'utf8') + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + return emptyConfig + } else { + throw err + } } - if (isAppRoot) { - rootConfig = result.filepath - } else if (result.filepath === rootConfig) { - // Make sure that custom plugins which don't have a config file won't cause - // the resolver to use the root config instead and start an infinite loop - // of config resolution. + + const configDoc = YAML.parseDocument(rawConfig, { customTags: [toolKitOption, toolKitIfDefined] }) + // go back and search for the parsed if-defined tag and include a string + // identifier so we can resolve all the tags in a JS object once we've loaded + // plugin options + YAML.visit(configDoc, { + Pair(_, pair) { + if (YAML.isScalar(pair.key) && pair.key.tag === '!toolkit/if-defined') { + // mangle the option name with the identifier so that multiple + // !toolkit/if-defined tags can be used in the same map with unique keys + return configDoc.createPair(`${toolKitIfDefinedIdent}${pair.key.value}`, pair.value) + } + } + }) + const config: RawRCFile = configDoc.toJS() ?? {} + + // if a toolkitrc contains a non-empty options field, but not options.{plugins,tasks,hooks}, + // assume it's an old-style, plugins-only options field. + // TODO:KB:20240410 remove this legacy options field handling in a future major version + if ( + config.options && + Object.keys(config.options).length > 0 && + !(config.options.plugins || config.options.tasks || config.options.hooks) + ) { + config.options = { + plugins: config.options as { [id: string]: Record } + } + logger.warn( - `plugin at ${s.filepath(path.dirname(root))} has no config file. please add an empty ${s.filepath( - '.toolkitrc' - )} file to avoid potential config resolution issues.` + `plugin at ${s.filepath(path.dirname(root))} has an ${s.code( + 'options' + )} field that only contains plugin options. these options should be moved to ${s.code( + 'options.plugins' + )}.` ) - return emptyConfig } - const config: RawRCFile = result.config return { + version: config.version ?? undefined, plugins: config.plugins ?? [], - hooks: config.hooks ?? {}, - options: config.options ?? {} + installs: config.installs ?? {}, + tasks: config.tasks ?? {}, + commands: config.commands ?? {}, + options: config.options + ? { + plugins: config.options.plugins ?? {}, + tasks: config.options.tasks ?? {}, + hooks: config.options.hooks ?? [] + } + : { plugins: {}, tasks: {}, hooks: [] }, + hooks: config.hooks ?? undefined, + init: config.init ?? [] } } diff --git a/core/cli/src/tasks.ts b/core/cli/src/tasks.ts new file mode 100644 index 000000000..a2b452994 --- /dev/null +++ b/core/cli/src/tasks.ts @@ -0,0 +1,152 @@ +import type { ValidConfig } from '@dotcom-tool-kit/config' +import { Task, TaskConstructor } from '@dotcom-tool-kit/base' +import { Validated, invalid, reduceValidated, valid } from '@dotcom-tool-kit/validated' +import type { Logger } from 'winston' +import { importEntryPoint } from './plugin/entry-point' +import { OptionKey, getOptions, setOptions } from '@dotcom-tool-kit/options' +import { loadConfig } from './config' +import { ToolKitError } from '@dotcom-tool-kit/error' +import { checkInstall } from './install' +import { styles } from '@dotcom-tool-kit/logger' +import { shouldDisableNativeFetch } from './fetch' +import { runInit } from './init' +import { formatInvalidOption } from './messages' +import { type TaskOptions, TaskSchemas } from '@dotcom-tool-kit/schemas' +import { OptionsForTask } from '@dotcom-tool-kit/plugin' + +type ErrorSummary = { + task: string + error: Error +} + +export async function loadTasks( + logger: Logger, + tasks: OptionsForTask[], + config: ValidConfig +): Promise> { + const taskResults = await Promise.all( + tasks.map(async ({ task: taskId, options, plugin }) => { + const entryPoint = config.tasks[taskId] + const taskResult = await importEntryPoint(Task, entryPoint) + + return taskResult.flatMap((Task) => { + const taskSchema = TaskSchemas[taskId as keyof TaskOptions] + const configOptions = config.taskOptions[taskId]?.options ?? {} + const mergedOptions = { ...configOptions, ...options } + const parsedOptions = taskSchema?.safeParse(mergedOptions) ?? { + success: true, + data: mergedOptions + } + + if (parsedOptions.success) { + const task = new (Task as unknown as TaskConstructor)( + logger, + taskId, + plugin, + getOptions(entryPoint.plugin.id as OptionKey) ?? {}, + parsedOptions.data + ) + return valid(task) + } else { + return invalid([formatInvalidOption([styles.task(taskId), parsedOptions.error])]) + } + }) + }) + ) + + return reduceValidated(taskResults) +} + +export function handleTaskErrors(errors: ErrorSummary[], command?: string) { + const error = new ToolKitError(`error running tasks for ${styles.command(command)}`) + error.details = errors + .map( + ({ task, error }) => + `${styles.heading(`${styles.task(task)}:`)} + +${error.message}${ + error instanceof ToolKitError + ? ` + +${error.details}` + : '' + }` + ) + .join(`${styles.dim(styles.ruler())}\n`) + + error.exitCode = errors.length + 1 + throw error +} + +export async function runTasks( + logger: Logger, + config: ValidConfig, + tasks: Task[], + command?: string, + files?: string[] +) { + const errors: ErrorSummary[] = [] + + if (tasks.length === 0) { + logger.warn(`no task configured for ${styles.command(command)}: skipping assignment...`) + } + + for (const task of tasks) { + try { + logger.info(styles.taskHeader(`running ${styles.task(task.id)} task`)) + await task.run({ files, config }) + } catch (error) { + // TODO use validated for this + // allow subsequent command tasks to run on error + errors.push({ + task: task.id, + error: error as Error + }) + } + } + + if (errors.length > 0) { + handleTaskErrors(errors, command) + } +} + +export async function runCommandsFromConfig( + logger: Logger, + config: ValidConfig, + commands: string[], + files?: string[] +): Promise { + for (const pluginOptions of Object.values(config.pluginOptions)) { + if (pluginOptions.forPlugin) { + setOptions(pluginOptions.forPlugin.id as OptionKey, pluginOptions.options) + } + } + + await runInit(logger, config) + await checkInstall(logger, config) + + if (shouldDisableNativeFetch()) { + process.execArgv.push('--no-experimental-fetch') + } + + const commandTasks = reduceValidated( + await Promise.all( + commands.map(async (command) => { + const tasks = config.commandTasks[command]?.tasks ?? [] + const validatedTaskInstances = await loadTasks(logger, tasks, config) + + return validatedTaskInstances.map((taskInstances) => ({ command, tasks: taskInstances })) + }) + ) + ).unwrap('tasks are invalid!') + + for (const { command, tasks } of commandTasks) { + await runTasks(logger, config, tasks, command, files) + } +} + +export async function runCommands(logger: Logger, commands: string[], files?: string[]): Promise { + const config = await loadConfig(logger) + + return runCommandsFromConfig(logger, config, commands, files) +} diff --git a/core/cli/test/files/conflict-resolution/.toolkitrc.yml b/core/cli/test/files/conflict-resolution/.toolkitrc.yml index 390ed216c..69eecfe01 100644 --- a/core/cli/test/files/conflict-resolution/.toolkitrc.yml +++ b/core/cli/test/files/conflict-resolution/.toolkitrc.yml @@ -5,25 +5,30 @@ plugins: - '@dotcom-tool-kit/heroku' # for build:remote hook and build:local via npm plugin options: - '@dotcom-tool-kit/heroku': - pipeline: tool-kit-test - systemCode: tool-kit-test - scaling: - tool-kit-test: - web: - size: standard-1x - quantity: 1 - '@dotcom-tool-kit/vault': - team: platforms - app: tool-kit-test + plugins: + '@dotcom-tool-kit/heroku': + pipeline: tool-kit-test + systemCode: tool-kit-test + '@dotcom-tool-kit/vault': + team: platforms + app: tool-kit-test + tasks: + HerokuProduction: + scaling: + tool-kit-test: + web: + size: standard-1x + quantity: 1 -hooks: +commands: build:local: - - WebpackDevelopment - - BabelDevelopment + - Webpack + - Babel build:ci: - - WebpackProduction - - BabelProduction + - Webpack + - Babel build:remote: - - WebpackProduction - - BabelProduction + - Webpack + - Babel + +version: 2 diff --git a/core/cli/test/files/conflicted/.toolkitrc.yml b/core/cli/test/files/conflicted/.toolkitrc.yml index b5aba1588..fa591e1a6 100644 --- a/core/cli/test/files/conflicted/.toolkitrc.yml +++ b/core/cli/test/files/conflicted/.toolkitrc.yml @@ -6,4 +6,6 @@ plugins: options: -hooks: +commands: + +version: 2 diff --git a/core/cli/test/files/cousins/.toolkitrc.yml b/core/cli/test/files/cousins/.toolkitrc.yml index 7555dd964..6d9e095f4 100644 --- a/core/cli/test/files/cousins/.toolkitrc.yml +++ b/core/cli/test/files/cousins/.toolkitrc.yml @@ -6,4 +6,6 @@ plugins: options: -hooks: +commands: + +version: 2 diff --git a/core/cli/test/files/duplicate/.toolkitrc.yml b/core/cli/test/files/duplicate/.toolkitrc.yml index 5cbb53077..15a2b4078 100644 --- a/core/cli/test/files/duplicate/.toolkitrc.yml +++ b/core/cli/test/files/duplicate/.toolkitrc.yml @@ -3,16 +3,22 @@ plugins: - '@dotcom-tool-kit/heroku' options: - '@dotcom-tool-kit/heroku': - pipeline: tool-kit-test - systemCode: tool-kit-test - scaling: - tool-kit-test: - web: - size: standard-1x - quantity: 1 - '@dotcom-tool-kit/vault': - team: platforms - app: tool-kit-test + plugins: + '@dotcom-tool-kit/heroku': + pipeline: tool-kit-test + systemCode: tool-kit-test -hooks: + '@dotcom-tool-kit/vault': + team: platforms + app: tool-kit-test + tasks: + HerokuProduction: + scaling: + tool-kit-test: + web: + size: standard-1x + quantity: 1 + +commands: + +version: 2 diff --git a/core/cli/test/files/invalid/.gitignore b/core/cli/test/files/invalid/.gitignore deleted file mode 100644 index cf4bab9dd..000000000 --- a/core/cli/test/files/invalid/.gitignore +++ /dev/null @@ -1 +0,0 @@ -!node_modules diff --git a/core/cli/test/files/invalid/.toolkitrc.yml b/core/cli/test/files/invalid/.toolkitrc.yml deleted file mode 100644 index 3438468d8..000000000 --- a/core/cli/test/files/invalid/.toolkitrc.yml +++ /dev/null @@ -1,2 +0,0 @@ -plugins: - - invalid diff --git a/core/cli/test/files/invalid/node_modules/invalid/index.js b/core/cli/test/files/invalid/node_modules/invalid/index.js deleted file mode 100644 index 3e0126f4f..000000000 --- a/core/cli/test/files/invalid/node_modules/invalid/index.js +++ /dev/null @@ -1,4 +0,0 @@ -class InvalidTask {} - -exports.tasks = [InvalidTask] -exports.hooks = [] diff --git a/core/cli/test/files/invalid/node_modules/invalid/package.json b/core/cli/test/files/invalid/node_modules/invalid/package.json deleted file mode 100644 index 1bcc6ba1c..000000000 --- a/core/cli/test/files/invalid/node_modules/invalid/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "invalid", - "version": "0.0.0", - "main": "index.js", - "license": "ISC" -} diff --git a/core/cli/test/files/multiple-hook-options/.toolkitrc.yml b/core/cli/test/files/multiple-hook-options/.toolkitrc.yml new file mode 100644 index 000000000..2d6ea68d1 --- /dev/null +++ b/core/cli/test/files/multiple-hook-options/.toolkitrc.yml @@ -0,0 +1,12 @@ +plugins: + - '@dotcom-tool-kit/circleci' + - '@dotcom-tool-kit/package-json-hook' +options: + hooks: + - PackageJson: + scripts: + somethingcustom: 'test:local' + - CircleCi: + jobs: + - name: test + command: test:local diff --git a/core/cli/test/files/successful/.toolkitrc.yml b/core/cli/test/files/successful/.toolkitrc.yml index a9ed03963..369899e0d 100644 --- a/core/cli/test/files/successful/.toolkitrc.yml +++ b/core/cli/test/files/successful/.toolkitrc.yml @@ -5,18 +5,21 @@ plugins: - '@dotcom-tool-kit/circleci-deploy' # test:local hook via npm plugin and test:ci hook options: - '@dotcom-tool-kit/eslint': - files: - - webpack.config.js - '@dotcom-tool-kit/mocha': - testDir: './mocha_tests' - '@dotcom-tool-kit/n-test': - host: 'https://example.com' + plugins: + '@dotcom-tool-kit/eslint': + files: + - webpack.config.js + '@dotcom-tool-kit/mocha': + testDir: './mocha_tests' + '@dotcom-tool-kit/n-test': + host: 'https://example.com' -hooks: +commands: 'test:local': - Mocha - Eslint 'test:ci': - Mocha - Eslint + +version: 2 diff --git a/core/cli/test/index.test.ts b/core/cli/test/index.test.ts index c91ec5638..fc8223fa6 100644 --- a/core/cli/test/index.test.ts +++ b/core/cli/test/index.test.ts @@ -1,32 +1,24 @@ import { ToolKitError } from '@dotcom-tool-kit/error' -import { Invalid, Plugin, Valid } from '@dotcom-tool-kit/types' +import type { Valid } from '@dotcom-tool-kit/validated' +import type { Plugin } from '@dotcom-tool-kit/plugin' +import type { ValidPluginsConfig } from '@dotcom-tool-kit/config' import { describe, expect, it, jest } from '@jest/globals' import * as path from 'path' import winston, { Logger } from 'winston' -import { createConfig, validateConfig, validatePlugins, ValidPluginsConfig } from '../src/config' -import { loadPlugin, resolvePlugin } from '../src/plugin' +import { createConfig, validateConfig } from '../src/config' +import { loadHookInstallations } from '../src/install' +import { loadPlugin, resolvePlugin, resolvePluginOptions } from '../src/plugin' +import { validatePlugins } from '../src/config/validate-plugins' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger // Loading all the plugins can (unfortunately) take longer than the default 2s timeout jest.setTimeout(20000) describe('cli', () => { - it('should report when plugins are invalid', async () => { - const config = createConfig() - - const plugin = await loadPlugin('app root', config, logger, { - id: 'invalid plugin test root', - root: path.join(__dirname, 'files/invalid') - }) - - expect(plugin.valid).toBe(false) - const reason = (plugin as Invalid).reasons[0] - expect(reason).toContain('type symbol is missing') - expect(reason).toContain('plugin is not an object') - }) - - it('should indicate when there are conflicts', async () => { + // TODO:KB:202301121 we only return conflicts for hooks that are defined. + // currently there are no hooks lol + it.skip('should indicate when there are conflicts', async () => { const config = createConfig() const plugin = await loadPlugin('app root', config, logger, { @@ -39,12 +31,13 @@ describe('cli', () => { expect(validatedPluginConfig.valid).toBe(true) const validPluginConfig = (validatedPluginConfig as Valid).value + resolvePluginOptions((plugin as Valid).value, validPluginConfig) resolvePlugin((plugin as Valid).value, validPluginConfig, logger) expect(() => validateConfig(validPluginConfig, logger)).toThrow(ToolKitError) - expect(validPluginConfig).toHaveProperty('hookTasks.build:ci.conflicting') - expect(validPluginConfig).toHaveProperty('hookTasks.build:remote.conflicting') - expect(validPluginConfig).toHaveProperty('hookTasks.build:local.conflicting') + expect(validPluginConfig).toHaveProperty('commandTasks.build:ci.conflicting') + expect(validPluginConfig).toHaveProperty('commandTasks.build:remote.conflicting') + expect(validPluginConfig).toHaveProperty('commandTasks.build:local.conflicting') }) it('should indicate when there are conflicts between plugins that are cousins in the tree', async () => { @@ -60,12 +53,13 @@ describe('cli', () => { expect(validatedPluginConfig.valid).toBe(true) const validPluginConfig = (validatedPluginConfig as Valid).value + resolvePluginOptions((plugin as Valid).value, validPluginConfig) resolvePlugin((plugin as Valid).value, validPluginConfig, logger) expect(() => validateConfig(validPluginConfig, logger)).toThrow(ToolKitError) - expect(config).toHaveProperty('hookTasks.build:ci.conflicting') - expect(config).toHaveProperty('hookTasks.build:remote.conflicting') - expect(config).toHaveProperty('hookTasks.build:local.conflicting') + expect(config).toHaveProperty('commandTasks.build:ci.conflicting') + expect(config).toHaveProperty('commandTasks.build:remote.conflicting') + expect(config).toHaveProperty('commandTasks.build:local.conflicting') }) it('should not have conflicts between multiple of the same plugin', async () => { @@ -81,12 +75,12 @@ describe('cli', () => { expect(validatedPluginConfig.valid).toBe(true) const validPluginConfig = (validatedPluginConfig as Valid).value + resolvePluginOptions((plugin as Valid).value, validPluginConfig) resolvePlugin((plugin as Valid).value, validPluginConfig, logger) try { const validConfig = validateConfig(validPluginConfig, logger) expect(validConfig).not.toHaveProperty('hooks.build:local.conflicting') - expect(validConfig.hooks['build:local'].plugin?.id).toEqual('@dotcom-tool-kit/npm') } catch (e) { if (e instanceof ToolKitError) { e.message += '\n' + e.details @@ -109,13 +103,17 @@ describe('cli', () => { expect(validatedPluginConfig.valid).toBe(true) const validPluginConfig = (validatedPluginConfig as Valid).value + resolvePluginOptions((plugin as Valid).value, validPluginConfig) resolvePlugin((plugin as Valid).value, validPluginConfig, logger) try { const validConfig = validateConfig(validPluginConfig, logger) - expect(validConfig).not.toHaveProperty('hookTasks.build:local.conflicting') - expect(validConfig.hookTasks['build:local'].tasks).toEqual(['WebpackDevelopment', 'BabelDevelopment']) + expect(validConfig).not.toHaveProperty('commandTasks.build:local.conflicting') + expect(validConfig.commandTasks['build:local'].tasks.map((task) => task.task)).toEqual([ + 'Webpack', + 'Babel' + ]) } catch (e) { if (e instanceof ToolKitError) { e.message += '\n' + e.details @@ -124,4 +122,24 @@ describe('cli', () => { throw e } }) + + it('should successfully install when options for different hooks are defined', async () => { + const config = createConfig() + + const plugin = await loadPlugin('app root', config, logger, { + id: 'reolved test root', + root: path.join(__dirname, 'files/multiple-hook-options') + }) + expect(plugin.valid).toBe(true) + + const validatedPluginConfig = validatePlugins(config) + expect(validatedPluginConfig.valid).toBe(true) + const validPluginConfig = (validatedPluginConfig as Valid).value + + resolvePlugin((plugin as Valid).value, validPluginConfig, logger) + + const validConfig = validateConfig(validPluginConfig, logger) + const hooks = await loadHookInstallations(logger, validConfig) + expect(hooks.valid).toBe(true) + }) }) diff --git a/core/cli/test/options.test.ts b/core/cli/test/options.test.ts new file mode 100644 index 000000000..6b98dd439 --- /dev/null +++ b/core/cli/test/options.test.ts @@ -0,0 +1,137 @@ +import { loadConfig } from '../src/config' + +import * as fs from 'node:fs/promises' + +import type { Valid } from '@dotcom-tool-kit/validated' +import type { Plugin } from '@dotcom-tool-kit/plugin' + +import winston, { type Logger } from 'winston' + +const logger = winston as unknown as Logger + +jest.mock('node:fs/promises') +const mockedFs = jest.mocked(fs) + +// convince text editors (well, nvim) to highlight strings as YAML +const yaml = (str) => str + +describe('option substitution', () => { + it('should substitute option tag with option value', async () => { + mockedFs.readFile.mockResolvedValueOnce( + yaml(` +options: + plugins: + test: + foo: bar + hooks: + - Test: + baz: !toolkit/option 'test.foo' +`) + ) + + const config = await loadConfig(logger, { validate: false }) + const plugin = config.plugins['app root'] + expect(plugin.valid).toBe(true) + expect((plugin as Valid).value.rcFile?.options.hooks[0].Test.baz).toEqual('bar') + }) + + it('should substitute defined tag with value when defined', async () => { + mockedFs.readFile.mockResolvedValueOnce( + yaml(` +options: + plugins: + test: + foo: bar + hooks: + - Test: + !toolkit/if-defined 'test.foo': + hello: world +`) + ) + + const config = await loadConfig(logger, { validate: false }) + const plugin = config.plugins['app root'] + expect(plugin.valid).toBe(true) + expect((plugin as Valid).value.rcFile?.options.hooks[0].Test.hello).toEqual('world') + }) + + it('should delete defined tag when not defined', async () => { + mockedFs.readFile.mockResolvedValueOnce( + yaml(` +options: + hooks: + - Test: + !toolkit/if-defined 'test.foo': + hello: world +`) + ) + + const config = await loadConfig(logger, { validate: false }) + const plugin = config.plugins['app root'] + expect(plugin.valid).toBe(true) + expect((plugin as Valid).value.rcFile?.options.hooks[0].Test.hello).toBeUndefined() + }) + + it('should support nested tags', async () => { + mockedFs.readFile.mockResolvedValueOnce( + yaml(` +options: + plugins: + test: + foo: bar + hooks: + - Test: + !toolkit/if-defined 'test.foo': + hello: !toolkit/option 'test.foo' +`) + ) + + const config = await loadConfig(logger, { validate: false }) + const plugin = config.plugins['app root'] + expect(plugin.valid).toBe(true) + expect((plugin as Valid).value.rcFile?.options.hooks[0].Test.hello).toEqual('bar') + }) + + it('should disallow tags within plugin options', async () => { + mockedFs.readFile.mockResolvedValueOnce( + yaml(` +options: + plugins: + test: + foo: !toolkit/option 'test.foo' +`) + ) + + expect(loadConfig(logger, { validate: false })).rejects.toThrowErrorMatchingInlineSnapshot( + `"cannot reference plugin options when specifying options"` + ) + }) + + it('should print multiple invalid tags in error', async () => { + mockedFs.readFile.mockResolvedValueOnce( + yaml(` +options: + plugins: + test: + !toolkit/if-defined 'test.foo': + foo: !toolkit/option 'test.foo' + other-test: + - bar: !toolkit/option 'test.bar' +`) + ) + + expect.assertions(2) + try { + await loadConfig(logger, { validate: false }) + } catch (error) { + expect(error.details.split('\n\n')).toHaveLength(3) + expect(error.details).toMatchInlineSnapshot(` + "YAML tag referencing options used at path 'options.plugins.test' + + YAML tag referencing options used at path 'options.plugins.test.foo' + + YAML tag referencing options used at path 'options.plugins.other-test.0.bar'" + `) + } + }) +}) diff --git a/core/cli/test/plugin.test.js b/core/cli/test/plugin.test.js deleted file mode 100644 index 8fc5e6b57..000000000 --- a/core/cli/test/plugin.test.js +++ /dev/null @@ -1,25 +0,0 @@ -/* - * File must be JavaScript as ts-jest transform breaks instanceof Hook test in - * instantiatePlugin function, apparently comparing two different versions of - * the Hook constructor when going up the plugin's prototype chain. - */ -const { describe, it } = require('@jest/globals') -const fs = require('fs') -const path = require('path') -const winston = require('winston') -const { validatePlugin } = require('../lib/plugin') - -const pluginDir = path.join(__dirname, '../../../plugins') - -function getPlugins() { - const pluginDirContents = fs.readdirSync(pluginDir, { withFileTypes: true }) - return pluginDirContents.filter((plugin) => plugin.isDirectory()).map((plugin) => plugin.name) -} - -describe.each(getPlugins())('%s integration test', (plugin) => { - const packagePath = path.join(pluginDir, plugin) - it('should be a valid plugin', () => { - const pluginPackage = require(packagePath) - validatePlugin(pluginPackage, winston) - }) -}) diff --git a/core/cli/tsconfig.json b/core/cli/tsconfig.json index b78cb0552..b5d7b0850 100644 --- a/core/cli/tsconfig.json +++ b/core/cli/tsconfig.json @@ -11,14 +11,24 @@ "path": "../../lib/logger" }, { - "path": "../../lib/types" + "path": "../../lib/plugin" + }, + { + "path": "../../lib/config" + }, + { + "path": "../../lib/validated" + }, + { + "path": "../../lib/conflict" + }, + { + "path": "../../lib/base" } ], "compilerOptions": { "outDir": "lib", "rootDir": "src" }, - "include": [ - "src/**/*" - ] + "include": ["src/**/*"] } diff --git a/core/cli/tsconfig.test.json b/core/cli/tsconfig.test.json new file mode 100644 index 000000000..1664c1a55 --- /dev/null +++ b/core/cli/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "resolveJsonModule": true + } +} diff --git a/core/create/package.json b/core/create/package.json index 00f0ebae8..84b7dac67 100644 --- a/core/create/package.json +++ b/core/create/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/create", - "version": "3.7.1", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "bin": "./bin/create-tool-kit", @@ -13,10 +13,11 @@ "dependencies": { "@aws-sdk/client-iam": "^3.282.0", "@aws-sdk/client-sts": "^3.282.0", - "@dotcom-tool-kit/doppler": "^1.1.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@octokit/rest": "^19.0.5", "@quarterto/parse-makefile-rules": "^1.1.0", "cli-highlight": "^2.1.11", @@ -30,7 +31,8 @@ "prompts": "^2.4.1", "simple-git": "^3.16.1", "tslib": "^2.3.1", - "yaml": "^2.2.1" + "yaml": "^2.2.1", + "zod": "^3.22.4" }, "repository": { "type": "git", @@ -44,21 +46,20 @@ "/files" ], "devDependencies": { - "@types/financial-times__package-json": "^1.9.0", + "@types/financial-times__package-json": "2.0.0-beta.0", "@types/lodash": "^4.14.185", "@types/node": "^16.18.23", "@types/node-fetch": "^2.6.2", "@types/pacote": "^11.1.3", "@types/prompts": "^2.0.14", - "cosmiconfig": "^7.0.1", - "dotcom-tool-kit": "^3.4.5", + "dotcom-tool-kit": "4.0.0-beta.5", "type-fest": "^3.13.1" }, "volta": { "extends": "../../package.json" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/core/create/src/bizOps.ts b/core/create/src/bizOps.ts index 1e325e6a0..9075d2672 100644 --- a/core/create/src/bizOps.ts +++ b/core/create/src/bizOps.ts @@ -1,6 +1,6 @@ import { DopplerEnvVars } from '@dotcom-tool-kit/doppler' import { rootLogger as winstonLogger } from '@dotcom-tool-kit/logger' -import { BizOpsData, BizOpsSystem } from '@dotcom-tool-kit/types/lib/bizOps' +import { BizOpsData, BizOpsSystem } from '@dotcom-tool-kit/schemas/lib/bizOps' import fetch from 'node-fetch' let bizOpsApiKey: string diff --git a/core/create/src/index.ts b/core/create/src/index.ts index 17192fd78..a3787e9e1 100644 --- a/core/create/src/index.ts +++ b/core/create/src/index.ts @@ -1,8 +1,7 @@ import * as ToolkitErrorModule from '@dotcom-tool-kit/error' import { rootLogger as winstonLogger, styles } from '@dotcom-tool-kit/logger' -import type { RCFile } from '@dotcom-tool-kit/types' +import type { RCFile } from '@dotcom-tool-kit/plugin' import { exec as _exec } from 'child_process' -import type { cosmiconfig } from 'cosmiconfig' import type { loadConfig as loadConfigType } from 'dotcom-tool-kit/lib/config' import fs, { promises as fsp } from 'fs' import importCwd from 'import-cwd' @@ -44,14 +43,6 @@ function getEslintConfigContent(): string { return eslintContentString } -function clearConfigCache() { - // we need to import explorer from the app itself instead of npx as this is - // the object used by installHooks() - return (importCwd('dotcom-tool-kit/lib/rc-file') as { - explorer: ReturnType - }).explorer.clearSearchCache() -} - async function executeMigration( deleteConfig: boolean, addEslintConfig: boolean, @@ -113,8 +104,11 @@ async function executeMigration( async function main() { const toolKitConfig: RCFile = { plugins: [], - hooks: {}, - options: {} + installs: {}, + tasks: {}, + commands: {}, + options: { plugins: {}, tasks: {}, hooks: [] }, + init: [] } const originalCircleConfig = await fsp.readFile(circleConfigPath, 'utf8').catch(() => undefined) @@ -168,7 +162,6 @@ async function main() { if (optionsCancelled) { return } - clearConfigCache() try { await catchToolKitErrorsInLogger(logger, installHooks(winstonLogger), 'installing Tool Kit hooks', true) } catch (error) { @@ -184,14 +177,12 @@ async function main() { if (conflictsCancelled) { return } - clearConfigCache() await catchToolKitErrorsInLogger( logger, installHooks(winstonLogger), 'installing Tool Kit hooks again', false ) - clearConfigCache() } else { throw error } diff --git a/core/create/src/prompts/conflicts.ts b/core/create/src/prompts/conflicts.ts index f26f3d588..e21086b32 100644 --- a/core/create/src/prompts/conflicts.ts +++ b/core/create/src/prompts/conflicts.ts @@ -1,7 +1,7 @@ import * as ToolkitErrorModule from '@dotcom-tool-kit/error' import { rootLogger as winstonLogger, styles } from '@dotcom-tool-kit/logger' -import type { RCFile } from '@dotcom-tool-kit/types' -import type { ValidConfig } from 'dotcom-tool-kit/lib/config' +import type { RCFile } from '@dotcom-tool-kit/plugin' +import type { ValidConfig } from '@dotcom-tool-kit/config' import type installHooksType from 'dotcom-tool-kit/lib/install' import { promises as fs } from 'fs' import importCwd from 'import-cwd' @@ -31,7 +31,7 @@ export default async ({ error, logger, toolKitConfig, configPath }: ConflictsPar for (const conflict of error.conflicts) { const remainingTasks = conflict.conflictingTasks - orderedHooks[conflict.hook] = [] + orderedHooks[conflict.command] = [] const totalTasks = remainingTasks.length for (let i = 1; i <= totalTasks; i++) { @@ -39,7 +39,7 @@ export default async ({ error, logger, toolKitConfig, configPath }: ConflictsPar name: 'order', type: 'select', message: `Hook ${styles.hook( - conflict.hook + conflict.command )} has multiple tasks configured for it, so an order must be specified. \ Please select the ${ordinal(i)} package to run.`, choices: [ @@ -57,12 +57,12 @@ Please select the ${ordinal(i)} package to run.`, break } else { const { task } = remainingTasks.splice(nextIdx, 1)[0] - orderedHooks[conflict.hook].push(task) + orderedHooks[conflict.command].push(task) } } } - toolKitConfig.hooks = orderedHooks + toolKitConfig.commands = orderedHooks const configFile = YAML.stringify(toolKitConfig) const { confirm } = await prompt({ diff --git a/core/create/src/prompts/oidc.ts b/core/create/src/prompts/oidc.ts index a55fb1d5c..aa72e9c3f 100644 --- a/core/create/src/prompts/oidc.ts +++ b/core/create/src/prompts/oidc.ts @@ -4,7 +4,7 @@ import { ToolKitError } from '@dotcom-tool-kit/error' import { rootLogger as winstonLogger, styles } from '@dotcom-tool-kit/logger' import { DopplerEnvVars } from '@dotcom-tool-kit/doppler' import { setOptions } from '@dotcom-tool-kit/options' -import type { RCFile } from '@dotcom-tool-kit/types' +import type { RCFile } from '@dotcom-tool-kit/plugin' import { Octokit } from '@octokit/rest' import * as suggester from 'code-suggester' import { highlight } from 'cli-highlight' @@ -177,12 +177,17 @@ export default async function oidcPrompt({ toolKitConfig }: OidcParams): Promise const dopplerEnvVars = new DopplerEnvVars(winstonLogger, 'prod', { project: 'dotcom-tool-kit' }) - const dopplerSecretsSchema = z.object({ - CIRCLECI_AUTH_TOKEN: z.string(), - GITHUB_ACCESS_TOKEN: z.string(), - [`AWS_${awsAccountDopplerName}_ACCESS_KEY_ID`]: z.string(), - [`AWS_${awsAccountDopplerName}_SECRET_ACCESS_KEY`]: z.string() - }) + const dopplerSecretsSchema = z + .object({ + CIRCLECI_AUTH_TOKEN: z.string(), + GITHUB_ACCESS_TOKEN: z.string() + }) + .merge( + z.object({ + [`AWS_${awsAccountDopplerName}_ACCESS_KEY_ID`]: z.string(), + [`AWS_${awsAccountDopplerName}_SECRET_ACCESS_KEY`]: z.string() + }) + ) const dopplerEnv = dopplerSecretsSchema.parse(await dopplerEnvVars.get()) let serverlessConfigRaw @@ -251,8 +256,8 @@ export default async function oidcPrompt({ toolKitConfig }: OidcParams): Promise // Kit vault plugin options. The class tries to read the options from // the global options object so let's set these options based on what's // been selected during the options prompt. - setOptions('@dotcom-tool-kit/vault', toolKitConfig.options['@dotcom-tool-kit/vault']) - setOptions('@dotcom-tool-kit/doppler', toolKitConfig.options['@dotcom-tool-kit/doppler']) + setOptions('@dotcom-tool-kit/vault', toolKitConfig.options.plugins['@dotcom-tool-kit/vault']) + setOptions('@dotcom-tool-kit/doppler', toolKitConfig.options.plugins['@dotcom-tool-kit/doppler']) const dopplerProjectName = new DopplerEnvVars(winstonLogger, 'prod').options.project const ssmAction = 'ssm:GetParameter' const ssmResource = `arn:aws:ssm:eu-west-1:\${AWS::AccountId}:parameter/${dopplerProjectName}/*` diff --git a/core/create/src/prompts/options.ts b/core/create/src/prompts/options.ts index 2b82503d2..8b2e46864 100644 --- a/core/create/src/prompts/options.ts +++ b/core/create/src/prompts/options.ts @@ -1,7 +1,7 @@ import { rootLogger as winstonLogger, styles } from '@dotcom-tool-kit/logger' -import type { RCFile } from '@dotcom-tool-kit/types' -import type { PromptGenerators } from '@dotcom-tool-kit/types/src/schema' -import type { RawConfig } from 'dotcom-tool-kit/lib/config' +import type { RCFile } from '@dotcom-tool-kit/plugin' +import type { RawConfig } from '@dotcom-tool-kit/config' +import type { PromptGenerators } from '@dotcom-tool-kit/schemas' import { promises as fs } from 'fs' import YAML from 'yaml' import type Logger from 'komatsu' @@ -51,7 +51,7 @@ async function optionsPromptForPlugin( { onCancel } ) if (stringOption !== '') { - toolKitConfig.options[plugin][optionName] = stringOption + toolKitConfig.options.plugins[plugin][optionName] = stringOption } break case 'ZodBoolean': @@ -67,7 +67,7 @@ async function optionsPromptForPlugin( }, { onCancel } ) - toolKitConfig.options[plugin][optionName] = boolOption + toolKitConfig.options.plugins[plugin][optionName] = boolOption break case 'ZodNumber': const { numberOption } = await prompt( @@ -80,7 +80,7 @@ async function optionsPromptForPlugin( { onCancel } ) if (numberOption !== '') { - toolKitConfig.options[plugin][optionName] = Number.parseFloat(numberOption) + toolKitConfig.options.plugins[plugin][optionName] = Number.parseFloat(numberOption) } break case 'ZodArray': @@ -98,7 +98,9 @@ async function optionsPromptForPlugin( { onCancel } ) if (stringArrayOption !== '' && stringArrayOption !== undefined) { - toolKitConfig.options[plugin][optionName] = stringArrayOption.split(',').map((s) => s.trim()) + toolKitConfig.options.plugins[plugin][optionName] = stringArrayOption + .split(',') + .map((s) => s.trim()) } break case 'ZodNumber': @@ -113,7 +115,7 @@ async function optionsPromptForPlugin( { onCancel } ) if (numberArrayOption !== '' && numberArrayOption !== undefined) { - toolKitConfig.options[plugin][optionName] = numberArrayOption + toolKitConfig.options.plugins[plugin][optionName] = numberArrayOption .split(',') .map((s) => Number.parseFloat(s.trim())) } @@ -135,7 +137,7 @@ async function optionsPromptForPlugin( { onCancel } ) if (option !== '') { - toolKitConfig.options[plugin][optionName] = option + toolKitConfig.options.plugins[plugin][optionName] = option } break } @@ -154,7 +156,7 @@ async function optionsPromptForPlugin( { onCancel } ) if (option !== '') { - toolKitConfig.options[plugin][optionName] = option + toolKitConfig.options.plugins[plugin][optionName] = option } break case 'ZodUnion': @@ -199,6 +201,8 @@ export default async ({ configPath, bizOpsSystem }: OptionsParams): Promise => { + toolKitConfig.options.plugins = {} + for (const plugin of Object.keys(config.plugins)) { let options: z.AnyZodObject let generators: PromptGenerators | undefined @@ -208,7 +212,7 @@ export default async ({ // TODO allow different schemas for tasks within a plugin const { Schema, generators: SchemaGenerators } = // eslint-disable-next-line @typescript-eslint/no-var-requires - require(`@dotcom-tool-kit/types/lib/schema/${pluginName}`) + require(`@dotcom-tool-kit/schemas/lib/plugins/${pluginName}`) options = Schema generators = SchemaGenerators } catch (err) { @@ -224,7 +228,6 @@ export default async ({ const anyRequired = required.length > 0 const styledPlugin = styles.plugin(pluginName) - toolKitConfig.options[plugin] = {} if (anyRequired) { winstonLogger.info(`Please now configure the options for the ${styledPlugin} plugin.`) @@ -238,7 +241,7 @@ export default async ({ * the object is partial because not all options for a plugin will * have generators, but all values in the record will be defined **/ - toolKitConfig.options[plugin][optionName] = await generator!( + toolKitConfig.options.plugins![plugin][optionName] = await generator!( winstonLogger.child({ plugin }), prompt, onCancel, @@ -256,12 +259,12 @@ export default async ({ plugin, required .map(([name, type]) => ({ name, type })) - .filter(({ name }) => !toolKitConfig.options[plugin][name]) + .filter(({ name }) => !toolKitConfig.options.plugins[plugin][name]) ) } if (cancelled) { - delete toolKitConfig.options[plugin] + delete toolKitConfig.options.plugins[plugin] return true } } @@ -286,7 +289,7 @@ export default async ({ }) ) } else if (!anyRequired) { - delete toolKitConfig.options[plugin] + delete toolKitConfig.options.plugins[plugin] } } } diff --git a/core/create/tsconfig.json b/core/create/tsconfig.json index 275fc45f7..55e3f7106 100644 --- a/core/create/tsconfig.json +++ b/core/create/tsconfig.json @@ -12,7 +12,13 @@ "path": "../../lib/logger" }, { - "path": "../../lib/types" + "path": "../../lib/config" + }, + { + "path": "../../lib/plugin" + }, + { + "path": "../../lib/schemas" }, { "path": "../cli" diff --git a/core/sandbox/.gitignore b/core/sandbox/.gitignore index fa454c38e..5c8ff07f9 100644 --- a/core/sandbox/.gitignore +++ b/core/sandbox/.gitignore @@ -1,3 +1,4 @@ * !readme.md !.gitignore +!jest.config.js diff --git a/core/sandbox/jest.config.js b/core/sandbox/jest.config.js new file mode 100644 index 000000000..73c77e8f5 --- /dev/null +++ b/core/sandbox/jest.config.js @@ -0,0 +1,4 @@ +// Jest seems to have a bug where a directory in its list of projects without a +// Jest config will cause all tests to be run twice. Let's leave a config here +// to prevent that. +module.exports = {} diff --git a/docs/concepts.md b/docs/concepts.md new file mode 100644 index 000000000..c359e7846 --- /dev/null +++ b/docs/concepts.md @@ -0,0 +1,41 @@ +# Tool Kit's core concepts + +## Plugins + + + +Tool Kit is a fully modular set of developer tooling. Not every project requires the same tooling, so to make sure different projects only have to install and configure what they need, Tool Kit is made up of several **plugins** that you can install separately to provide different groups of functionality, like [the `npm` plugin](plugins/npm), which lets Tool Kit manage things like `package.json` scripts. + +This means a project that uses Jest for its tests can install [the `jest` plugin](../plugins/jest), and a project using Mocha can install [the `mocha` plugin](plugins/mocha), and be able to run them consistently anywhere they're needed, e.g. the `npm run test` script, without having to configure that manually. Plugins can depend on other plugins, so we also publish plugins like [`frontend-app`](../plugins/frontend-app/) that bundle up most of the tooling you'll need for a particular use case  into a single package. + +And if there's something you want to use in your repo that's not yet supported by Tool Kit, you can [extend it](./extending-tool-kit.md) by writing a custom plugin that works consistently with any officially-supported tooling. + +Plugins provide **tasks**, which provide the code for running external tooling, and **hooks**, which manage configuration files in your repo that will be running tooling. They can also configure default **commands** that will run tasks. + +## Commands + + + +**Commands** are labels (like `test:local`) that you provide on the command line when running `dotcom-tool-kit`. They're assigned by `.toolkitrc.yml` files in your repo and in plugins to run [tasks](#tasks). + +Plugins can set a default command for their tasks to run on; for example, the `Jest` task [runs by default on the `test:local` command](./plugins/jest/.toolkitrc.yml#L5). If you've got multiple tasks trying to run on the same command by default, you'll need to [configure which you want to run](./docs/resolving-plugin-conflicts.md). + +## Tasks + +A **task** is a lightweight abstraction for running some tooling from outside of Tool Kit. Although most tasks simply make a single call to a third-party library or CLI tool, some are more complex, orchestrating the logic for things like our deployment process for Heroku. + +Tasks are written in TypeScript, so we can make use of modern Javascript-based tooling and libraries, easily provide structured logging and actionable error messages, and debug and maintain them more easily than things like Bash scripts. + +An example of a task is `Jest` from the [`jest` plugin](../plugins/jest), which abstracts running Jest tests in a local development environment. Some tasks support [configuration](../readme.md#configuring-tool-kit). This doesn't replace any native configuration that tooling might have (like a `jest.config.js`). + +## Hooks + +A **hook** manages configuration in your repo that will be running Tool Kit commands. Things like scripts in `package.json` or jobs in your CircleCI config can be automatically managed and kept consistent by hooks. + +For example, `package-json-hook` plugin provides a `PackageJson` hook that lets other plugins define npm scripts and other configuration. It's used by the `npm` plugin to configure things like the `test` script to run `dotcom-tool-kit test:local`. Any tasks that are configured to run on `test:local` will then be run when you run `npm run test`. + + + +Hooks are there to be **installed** in your repository. Hook classes contain an `install` method that updates the relevant configuration files to run that hook. This `install` method is called when you run `npx dotcom-tool-kit --install`. This lets Tool Kit plugins automatically manage files like `package.json` or `.circleci/config.yml`. Any changes made by hook installation should be committed. + +When Tool Kit starts up, it checks whether the hooks in your plugins are correctly installed, and will print an error if they're not. This prevents repos from getting out of sync with what Tool Kit expects, ensuring repos are fully consistent and correctly managed by plugins. diff --git a/docs/custom-plugins.md b/docs/custom-plugins.md deleted file mode 100644 index 66ab38efb..000000000 --- a/docs/custom-plugins.md +++ /dev/null @@ -1,142 +0,0 @@ -# Creating a custom Tool Kit plugin - -If your app requires some tooling that's not part of Tool Kit, you can write a custom plugin for that feature, which can work seamlessly together with the core Tool Kit plugins. This is the **only supported way** of using tooling that Tool Kit doesn't currently include. - -If you're looking to implement tooling in your repository that would require things like custom `npm` scripts, Bash scripts, or editing the Tool Kit-managed CircleCI config, **you should be writing a custom plugin**. - -A custom plugin can be written for a single repo or distributed as an npm package to be consumed by multiple repos owned by your team. The custom plugins themselves will be maintained and supported **by your team**, not Platforms. - -If there's wide demand for a particular custom plugin (for example, if it starts being used across multiple teams), we will consider adopting that plugin into Tool Kit. Writing a custom plugin (rather than implementing the tooling another way) will make it much more likely for us to be able to add the feature to Tool Kit. - -## Common plugin structure - -We recommend creating a `toolkit` folder at the root of your repository to contain your custom plugins, and folders inside that for each plugin. Each plugin folder should contain at least **an empty `.toolkitrc.yml` file** and **an empty `index.js`**. - -Let's say you're creating a plugin to run [Rollup](https://rollupjs.org). Your folder structure should look like this: - -``` -└ toolkit - └ rollup - ├ .toolkitrc.yml - └ index.js -``` - -This plugin can then be included in your top-level `.toolkitrc.yml` by referencing it as a relative path: - -```yml -plugins: - - './toolkit/rollup' -``` - -## Creating a task to be run by an existing hook - -Consider the tooling you're implementing, and when you'd expect it to run. Tool Kit likely already has the hooks included for those scenarios, so have a look at the core plugins to see if there's something similar, and look at what hooks it runs on by default in its `.toolkitrc.yml`. - -For something like Rollup, you'd probably expect it to run for local development, on continuous integration builds so you can run your tests, and when building an app so it can run in review or production. Tool Kit has a [built-in Webpack](https://github.com/Financial-Times/dotcom-tool-kit/tree/main/plugins/webpack) plugin; since it's also a bundler, like Rollup, it's a good example to compare to for your custom plugin. - -The Webpack plugin [runs by default](https://github.com/Financial-Times/dotcom-tool-kit/blob/main/plugins/webpack/.toolkitrc.yml) on the `build:local`, `build:ci`, and `build:remote` hooks, which sound like exactly the hooks you're looking for. - -To get Rollup to run at these points, you'll need to create a subclass of the `Task` class from `@dotcom-tool-kit/types`, implement the `run` method, and export it in an array of `tasks`. Your `toolkit/rollup/index.js` might look like this: - -```js -const { Task } = require('@dotcom-tool-kit/types') -const rollup = require('rollup') -const loadConfigFile = require('rollup/dist/loadConfigFile') -const path = require('path') - -class Rollup extends Task { - async run() { - const config = path.join(process.cwd(), 'rollup.config.js') - const { options, warnings } = await loadConfigFile(config) - - // print any config warnings to the console - warnings.flush() - - for (const optionsEntry of options) { - const bundle = await rollup.rollup(optionsEntry) - await Promise.all(optionsEntry.output.map(bundle.write)) - } - } -} - -exports.tasks = [Rollup] -``` - -Then, in the plugin's `.toolkitrc.yml`, list this task as the default task to run on the hooks you need: - -```yml -hooks: - 'build:local': Rollup - 'build:ci': Rollup - 'build:remote: Rollup -``` - -You should install `@dotcom-tool-kit/types` and the tooling you're implementing as `devDependencies` of your repo (e.g. `npm install --save-dev @dotcom-tool-kit/types rollup`). - -## Implementing a new hook - -A Hook defines an abstract label to run tasks with, as well as managing where in other project configuration it's run from. For example, the built-in `circleci` plugin defines a `build:ci` hook, which lets tasks like Rollup run in CI, and it specifies that `build:ci` should be run by a CircleCI job and automatically manages the configuration in `.circleci/config.yml` to run that job. - -This abstraction lets us write different plugins for defining tasks to be run, separate from the plugins defining where they should be run from, whilst maintaining the link between them. We've already seen that `build:ci` could be running Rollup, or Webpack, or any other task; in addition, `build:ci` itself could be defined by a different plugin, such as a Github Actions plugin, that would automatically manage configuration in `.github/workflows`. - -The automatic configuration management is implemented by `Hook` subclasses. These define a `check` method that should return `true` if the hook is correctly installed in the repository or `false` if it needs installing, and an `install` method to actually perform the installation. Every time Tool Kit runs, it checks that every hook is installed in your repo, and if any aren't, it exits with an error (to ensure the repo is always consistent with what it expects). You can then run `dotcom-tool-kit --install` to run the installation of every hook that isn't installed. - -If you find yourself asking a question like "how do I run a Tool Kit task from a different npm script", **you should implement a hook** to allow Tool Kit to automatically manage that configuration for any new repos using your plugin, rather than expecting new users to add that configuration themselves when installing the plugin. - -Hooks have a loose naming convention of `category:environment`. This is only meant for humans to be able to intuitively understand which hooks are related; it's not required by the Tool Kit core itself. - -Let's say you want to run some task on the npm `prepare` script (which automatically runs after `npm install` and before `npm publish`). We'll call that hook `prepare:local`, and the plugin will live in `toolkit/npm-prepare` ([structured as above](#common-plugin-structure)). Create a subclass of the `Hook` class from `@dotcom-tool-kit/types`, implement the `check` and `install` methods, and export a `hooks` object to map it to the name we're giving it. Your `toolkit/npm-prepare/index.js` might include: - -```js -const { Hook } = require('@dotcom-tool-kit/types') -const loadPackageJson = require('@financial-times/package-json') - -class PrepareHook extends Hook { - get packageJson() { - if (!this._packageJson) { - const filepath = path.resolve(process.cwd(), 'package.json') - this._packageJson = loadPackageJson({ filepath }) - } - - return this._packageJson - } - - async check() { - return this.packageJson.getField('scripts')?.prepare === 'dotcom-tool-kit prepare:local' - } - - async install() { - this.packageJson.requireScript({ - stage: 'prepare', - command: 'dotcom-tool-kit prepare:local' - }) - - this.packageJson.writeChanges() - } -} - -export const hooks = { - 'prepare:local': PrepareHook -} -``` - -There are a handful of common base classes that Tool Kit includes for common hook usecases (such as CircleCI configuration or npm `package.json` scripts) that you can use, instead of implementing your hook completely from scratch. For example, we can build our `prepare:local` hook on top of the `PackageJsonScriptHook` built-in class: - -```js -const { PackageJsonScriptHook } = require('@dotcom-tool-kit/package-json-hook') - -class PrepareHook extends PackageJsonScriptHook { - key = 'prepare' - hook = 'prepare:local' -} -``` - -After you've implemented your hook, running `dotcom-tool-kit --install` will add a `prepare` script to your `package.json`. - -## Summary - -- You can create new custom **tasks** to run tooling that Tool Kit doesn't support yet -- You can create custom **hooks** to run Tool Kit tasks in new scenarios -- Please [talk to the Platforms team](https://financialtimes.slack.com/archives/C02TRE2V2Q1) if you need help writing a custom plugin, want to discuss your use case, or to propose new features you think might be useful across multiple repositories & teams - -✌️ diff --git a/docs/developing-tool-kit.md b/docs/developing-tool-kit.md index 193d4dd6f..69f9383ac 100644 --- a/docs/developing-tool-kit.md +++ b/docs/developing-tool-kit.md @@ -22,6 +22,56 @@ The script will create the plugin folder and add all the necessary configuration At the root of the repository, `npm run watch` will run the Typescript compiler and build files when you change them. It's recommended to leave that running while you develop things. +## How Tool Kit loads plugins + +The Tool Kit CLI works by recursively loading plugins, merging them, depth-first, into a single [`config` object](../lib/config/src/index.ts), while labelling any conflicts between plugins, and allowing their parent plugins to potentially resolve conflicts. + +As far as the CLI is concerned, a "plugin" is anything with a `.toolkitrc.yml`. This means a user's repo is considered a plugin, which we label `app root` in the config. It's loaded as the first plugin, and all other plugins are loaded as its descendents. + +The Tool Kit CLI initialises in two phases: **loading** plugins, then **resolving** them. + +When **loading** a plugin, we parse its `.toolkitrc.yml`, builds a [`plugin` object](../lib/plugin/src/index.ts), and with the `plugins` array from the `.toolkitrc.yml`, load its children. + +When **resolving** a plugin, first we resolve its children (i.e. depth-first recursion). Then we merge its tasks, hooks, commands, plugin options, task options, and init functions (in that order) into the `config`. + +These all have slightly different logic for what's considered a "conflict", but in general, if a plugin tries to store something with a particular name in the config, and it's already been stored by something that isn't a descendent of this plugin, that's a conflict. + +On the other hand, if there's already a conflict in the config that _did_ come from a descendent of the current plugin, this plugin will replace the conflict with what it's currently trying to store, allowing parents to override their children to resolve conflicts. + +For an example, consider this (simplified) dependency tree of plugins: + +``` +└ app root + ├ frontend-app + │ ├ backend-heroku-app + │ │ ├ circleci + │ │ └ heroku + │ └ webpack + └ heroku +``` + +The plugins will be loaded in this order: + +1. `app root` +2. `frontend-app` +3. `backend-heroku-app` +4. `circleci` +5. `heroku` +6. `webpack` +7. (`heroku` has already been loaded, so is skipped here) + +And then resolved in this order: + +1. `circleci` +2. `heroku` +3. `backend-heroku-app` +4. `webpack` +5. `frontend-app` +6. (`heroku` has already been resolved, so is skipped here) +7. `app root` + +This depth-first resolution together with the `app root` being the ultimate ancestor plugin means a repo's `.toolkitrc.yml` can override anything (including conflicts) from any plugin, allowing users to have the final say over what's running when and how it's configured. + ## Plugin structure Tool Kit plugins are Node modules. Any code in the entry point of the plugin will be run when Tool Kit starts up and loads the plugin. You can use this for any initialisation the plugin needs to do, e.g. writing [state](#state) based on the environment. The module can export an array of [tasks](#tasks) and an object of [hooks](#hooks). @@ -36,8 +86,6 @@ A task extends the class `Task` from `@dotcom-tool-kit/types`, implementing its import { Task } from '@dotcom-tool-kit/types' export default class Webpack extends Task { - static description = 'bundle your code with webpack' - async run(): Promise { // call third-party tooling } @@ -58,7 +106,7 @@ Tasks won't be usable by your plugin's users unless you export them from the ent A hook ensures a repo using Tool Kit has the relevant configuration to run things from Tool Kit. -A hook extends the `Hook` class from `@dotcom-tool-kit/types`, implementing its abstract asynchronous `check` and `install` functions. You also need to write a helpful `description` field, which will be displayed in the `--help` text. +A hook extends the `Hook` class from `@dotcom-tool-kit/types`, implementing its abstract asynchronous `isInstalled` and `install` functions. You also need to write a helpful `description` field, which will be displayed in the `--help` text. ```typescript import { Hook } from '@dotcom-tool-kit/types' @@ -66,7 +114,7 @@ import { Hook } from '@dotcom-tool-kit/types' export default NpmRunTest extends Hook { static description = 'hook to run tasks with `npm run test`' - async check(): Promise { + async isInstalled(): Promise { // return true if the `test` script is correctly defined in `package.json` } @@ -94,7 +142,7 @@ This lets different plugins define the same abstractly labelled hooks with diffe #### Defining options -Plugins can define options that a user can configure in their repo's `.toolkitrc.yml`. We use the [`zod` library](https://zod.dev) to specify the schema, which allows us to define what we expect the options to look like and use this specification to validate the options we receive as well as generate TypeScript types for them. Options are defined in the `@dotcom-tool-kit/types` package, in the `schema` files. Create a file in [`src/schema`](../lib/types/src/schema) for your plugin, which should export a `NameOfPluginSchema` object (that should also be exported as `Schema`), and a `NameOfPluginOptions` type that uses the `SchemaOutput` generic type. +Plugins can define options that a user can configure in their repo's `.toolkitrc.yml`. We use the [`zod` library](https://zod.dev) to specify the schema, which allows us to define what we expect the options to look like and use this specification to validate the options we receive as well as generate TypeScript types for them. Options are defined in the `@dotcom-tool-kit/types` package, in the `schema` files. Create a file in [`src/schema`](../lib/schema/lib/plugins) for your plugin, which should export a `NameOfPluginSchema` object (that should also be exported as `Schema`), and a `NameOfPluginOptions` type that uses the `SchemaOutput` generic type. ```typescript import { z } from 'zod' @@ -136,9 +184,9 @@ To avoid boilerplate for tasks (the most common use case for options), when defi ```typescript import { Task } from '@dotcom-tool-kit/types' -import { ESLintOptions, ESLintSchema } from '@dotcom-tool-kit/types/lib/schema/eslint' +import { ESLintOptions, ESLintSchema } from '@dotcom-tool-kit/schemas/lib/plugins/eslint' -export default class Eslint extends Task { +export default class Eslint extends Task<{ plugin: typeof ESLintSchema }> { static description = '' async run(): Promise { @@ -157,6 +205,9 @@ Look at the [`state` package](../lib/state/) to see how to define, read and writ ## General philosophy +> [!TIP] +> See also the higher-level [Tool Kit principles](./tool-kit-principles.md). + - The Tool Kit core (`cli/core` and the packages it depends on) should **never** depend on any particular plugin. This would prevent users from using alternatives to that plugin. If you find yourself needing to add something to the core for a particular plugin, think about how other plugins would work with it, and make sure what you're writing is general enough for any similar plugin to work with it. diff --git a/docs/extending-tool-kit.md b/docs/extending-tool-kit.md new file mode 100644 index 000000000..b4f7b3b07 --- /dev/null +++ b/docs/extending-tool-kit.md @@ -0,0 +1,136 @@ +# Extending Tool Kit + +Tool Kit provides a development workflow for tooling that's common to most Customer Products projects. But almost every project has unique requirements that the default set of plugins and configuration in Tool Kit doesn't cover. For these use cases, there are various ways you can extend Tool Kit. + +You don't have to use Tool Kit for every tooling use case in your project. However, using Tool Kit for your custom tooling makes it more likely that it will be shareable between multiple projects and teams and potentially adopted into Tool Kit itself. + +## Running existing Tool Kit tasks in new scenarios + +In your repo's `.toolkitrc.yml`, you can define new commands to run tasks: + +```yml +commands: + build:e2e: Webpack +``` + +This can be run with `npx dotcom-tool-kit build:e2e`. + +A task's options can be set on a per-command basis, allowing the same task to be used for multiple use cases: + +```yml +commands: + build:local: + Webpack: + configFile: webpack.config.js + build:e2e: + Webpack: + configFile: webpack.e2e.config.js +``` + +Tool Kit **hooks** manage files like `package.json` and `.circleci/config.yml` to run commands from your existing tooling like npm scripts and CircleCI jobs. Hooks can be configured in your `.toolkitrc.yml` to integrate your new commands: + +```yml +options: + hooks: + - PackageJson: + scripts: + e2e: build:e2e + - CircleCIConfig: + jobs: + - name: build-e2e + command: build:e2e + workflows: + - name: 'tool-kit' + jobs: + - name: build-e2e + requires: + - setup +``` + +Adding this configuration and running `npx dotcom-tool-kit --install` will install the new npm script and CircleCI job into your repo, alongside the existing configuration from your Tool Kit plugins. + +## Creating a custom Tool Kit plugin + +If your app requires some tooling that's not provided by a first-party Tool Kit plugin, you can write a custom plugin for that feature, which works seamlessly together with the core Tool Kit plugins. + +A custom plugin can be written for a single repo or distributed as an npm package to be consumed by multiple repos owned by your team. The custom plugins themselves will be maintained and supported **by your team**, not Platforms, although we can . + +If there's wide demand for a particular custom plugin (for example, if it starts being used across multiple teams), we will consider adopting that plugin into Tool Kit. Writing a custom plugin (rather than implementing the tooling another way) will make it much more likely for us to be able to add the feature to Tool Kit. + +### Common plugin structure + +We recommend creating a `tool-kit` folder at the root of your repository to contain your custom plugins, and folders inside that for each plugin. Each plugin folder must contain at least a `.toolkitrc.yml` file. + +The `.toolkitrc.yml` is the manifest that tells the Tool Kit plugin loader what your plugin needs to load. It must contain a `version` field (Tool Kit will currently only load a version `2` plugin), and for custom plugins will probably need a `tasks` field telling Tool Kit to load your custom tasks. + +Let's say you're creating a plugin to run [Rollup](https://rollupjs.org). Your folder structure should look like this: + +``` +└ tool-kit + └ rollup + ├ .toolkitrc.yml + └ tasks + └ rollup.js +``` + +The `.toolkitrc.yml` would then contain: + +```yml +version: 2 + +tasks: + Rollup: ./tasks/rollup.js +``` + +This plugin can then be included in your top-level `.toolkitrc.yml` by referencing it as a relative path: + +```yml +plugins: + - './tool-kit/rollup' +``` + +### Writing the task + +Create a subclass of the `Task` class from `@dotcom-tool-kit/base`, implement the `run` method, and export it as the default export of the task module. + +You'll need to install `@dotcom-tool-kit/base` and the tooling you're implementing as `devDependencies` of your repo (e.g. `npm install --save-dev @dotcom-tool-kit/base rollup`). + +Your `tool-kit/rollup/index.js` might look like this: + +```js +const { Task } = require('@dotcom-tool-kit/base') +const rollup = require('rollup') +const loadConfigFile = require('rollup/dist/loadConfigFile') +const path = require('path') + +class Rollup extends Task { + async run() { + const config = path.join(process.cwd(), 'rollup.config.js') + const { options, warnings } = await loadConfigFile(config) + + // print any config warnings to the console + warnings.flush() + + for (const optionsEntry of options) { + const bundle = await rollup.rollup(optionsEntry) + await Promise.all(optionsEntry.output.map(bundle.write)) + } + } +} + +module.exports = Rollup +``` + +Then, in the plugin's `.toolkitrc.yml`, you can provide the default commands this task will run on. It's preferable to do this in the plugin `.toolkitrc.yml` instead of your top-level `.toolkitrc.yml` so your plugin is self-contained and can be more easily moved into its own repo or Tool Kit itself, if it's something that can be shared between multiple repos/teams. + +```yml +version: 2 + +tasks: + Rollup: ./tasks/rollup.js + +commands: + 'build:local': Rollup + 'build:ci': Rollup + 'build:remote': Rollup +``` diff --git a/docs/faq.md b/docs/faq.md deleted file mode 100644 index b8c33c292..000000000 --- a/docs/faq.md +++ /dev/null @@ -1,83 +0,0 @@ - -# Tool Kit FAQs - -Below are responses to some general questions about Tool Kit that have been asked during migrations. If your question is not answered here please ask in the [#tool-kit](https://app.slack.com/client/T025C95MN/C02TRE2V2Q1/thread/C042NBBTM-1671107986.307219) slack channel and consider adding it, and the answer, to this document. - -## How do I add package specific configuration (for e.g. ESLint) to the Tool Kit plugin (e.g. @dotcom-tool-kit/eslint))? - -As much as possible Tool Kit doesn't handle configuration for third party packages. The purpose of Tool Kit’s plugins for packages is to get them working in the FT development workflow with the minimum possible configuration. Tool Kit plugins will leave package configuration to a specific configuration file for that package (e.g. `.eslintrc` for the ESLint package). - -## How can I use custom commands for my tooling that are not accommodated for by Tool Kit's config? - -You don't need to use Tool Kit for everything. Tool Kit handles common tooling use cases that are required for most apps to work in the Customer Products ecosystem, but it's fine for your app to include additional tooling features that don't use Tool Kit at all. - -See [How do I run watch mode for tests with Tool Kit?](#how-do-i-run-watch-mode-for-tests-with-tool-kit) for a specific example of calling a third party package that Tool Kit configures without using Tool Kit at all. - -## How do I run watch mode for tests with Tool Kit? - -Now that we manage tasks with npm scripts, you can define a script to run your tests in watch mode: - -```json -{ - "scripts": - { - "watch-tests": "jest --watch" - } -} -``` - -Then run `npm run watch-tests` to execute your script. - -You can also use the package directly with npx to run your test runner with any options you wish: - -```sh -npx jest path/to/tests --watch -``` - -Note, this is an example of a script that doesn't use Tool Kit at all, and that's fine. - -## Is there a central list of all hooks and tasks available in Tool Kit? - -Funny you should ask! We have just the thing: - -| Hook | Hook exported by plugin | Possible tasks | -|---------------------|-------------------------|----------------------| -| `run:local` | npm | `Node` | -| | | `Nodemon` | -| | | `NextRouter` | -| | | `WebpackDevelopment` | -| | | `WebpackWatch` | -| | | `TypeScriptWatch` | -| `test:local` | npm | `Mocha` | -| | | `JestLocal` | -| | | `Eslint` | -| | | `CypressLocal` | -| | | `TypeScriptTest` | -| `build:local` | npm | `BabelDevelopment` | -| | | `WebpackDevelopment` | -| | | `TypeScriptBuild` | -| `build:ci` | circleci | `BabelProduction` | -| | | `WebpackProduction` | -| `test:ci` | circleci | `Eslint` | -| | | `Mocha` | -| | | `JestCi` | -| | | `CypressCi` | -| `deploy:review` | circleci-deploy | `HerokuReview` | -| `deploy:staging` | circleci-deploy | `HerokuStaging` | -| `test:review` | circleci-deploy | `NTest` | -| | | `Pa11y` | -| `test:staging` | circleci-deploy | `NTest` | -| `teardown:staging` | circleci-deploy | `HerokuTeardown` | -| `deploy:production` | circleci-deploy | `HerokuProduction` | -| `publish:tag` | circleci-npm | `NpmPublish` | -| `cleanup:remote` | heroku | `NpmPrune` | -| `release:remote` | heroku | `UploadAssetsToS3` | -| `build:remote` | heroku | `BabelProduction` | -| | | `WebpackProduction` | -| `git:precommit` | husky-npm | `LintStaged` | -| `git:commitmsg` | husky-npm | | -| `test:staged` | lint-staged-npm | `Eslint` | -| `format:staged` | lint-staged-npm | `Prettier` | -| `format:local` | prettier | `Prettier` | - -[table updated 4th January 2023] diff --git a/docs/migrating-to-tool-kit.md b/docs/migrating-to-tool-kit.md deleted file mode 100644 index b12ab547c..000000000 --- a/docs/migrating-to-tool-kit.md +++ /dev/null @@ -1,286 +0,0 @@ -# Migrating to Tool Kit from n-gage - -## Introduction - -Tool Kit is our suite of developer tooling for FT.com applications and -components. Tool Kit is designed to handle all parts of your workflow, from -building to testing to deploying. You can read more about the structure and -philosophy of Tool Kit in the [README](../README.md). - -Previously, [n-gage](https://github.com/Financial-Times/n-gage) was the main -tool used in the FT to orchestrate projects, and Tool Kit has been designed as -its replacement. This migration guide focuses on how to migrate from n-gage to -Tool Kit and so will give analogies and guidance based on it. Therefore, this -text is recommended only for projects already using n-gage, though it _may_ help -push you in the right direction if you're creating a fresh, new project. - -## Prerequisites - -Prior to using the migration tool, please make sure you are using npm 7 or above. - -## The Migration Tool - -We have created an interactive tool that will hopefully automate most of the -dull refactoring required to hook up your project with Tool Kit. The tool tries -to be as transparent about what it's doing as possible so should be easy enough -to follow along with. - -The migration tool is published to npm as `@dotcom-tool-kit/create`. Thanks to -some [syntax sugar -magic](https://docs.npmjs.com/cli/v7/commands/npm-init#description) provided by -npm's `npm init` script this means you can run - -```shell -npm init @dotcom-tool-kit@latest -``` - -within your repository to fetch the tool and start migrating. We include the -`@latest` specifier here to make sure you're using the latest version of the -migration tool, as `npx` has a particularly aggressive cache. Answer the -questions posed by the tool's prompts to configure Tool Kit in a way that's -suitable for your app or service. Let's now look in detail at each of the steps -in the migration process. - -## Step-By-Step - -The Tool Kit migration tool will ask you a series of questions about your -project and how much you want to immediately migrate. Let's copy each prompt -here and break down what they mean, why we need it, and what it will do. Note -that you might not see all these prompts in your particular run depending on how -complex the migration is. - -### What Kind Of App? - -``` -What kind of app is ${app}? -- A user-facing (frontend) app -- A service/backend app -``` - -The first question is key! Here you choose what kind of app you're migrating, -which will dictate many of the plugins that will be pulled in. Frontend apps -will use build tools like Webpack to bundle their code for the browser whilst -backend apps and services might use node and npm. These plugins are all bundled -up in a 'compilation' plugin called something like -`@dotcom-tool-kit/frontend-app` whose sole job is to list a set of other plugins -to include. - -### Plugins - -``` -Would you like to install any additional plugins? -- Jest -- Mocha -- ESLint -- Prettier -- lint-staged -``` - -You are then given the opportunity to add various plugins for other tools you -use in your project. It makes sense to choose all the ones you are already -using, but now is also a good chance to try out new tools you've been meaning to -integrate into your stack, such as `dotcom-tool-kit/prettier`! Note: if you don't already have Prettier installed it may be initially a little disruptive with many formatting changes. - -### ESLint Config - -``` -Would you like to add a default eslint config file at ./eslintrc.js? -``` - -Selecting "yes" will generate an `eslintrc.js` file at your project's root directory, which extends the ESLint shared configuration [`@financial-times/eslint-config-next`](https://github.com/Financial-Times/eslint-config-next), and install `@financial-times/eslint-config-next` for you automatically. If you've previously selected `@dotcom-tool-kit/frontend-app` for your app, the generated `.eslintrc.js` file will look like [this](https://github.com/Financial-Times/next-user-facing-app-template/blob/main/.eslintrc.js), whereas if you've selected `@dotcom-tool-kit/backend-heroku-app`, the generated `.eslintrc.js` file will look like [this](https://github.com/Financial-Times/next-service-app-template/blob/main/.eslintrc.js). - -### CircleCI Config - -``` -Would you like a CircleCI config to be generated? This will overwrite the current config at .circleci/config.yml. -``` - -Tool Kit provides a CircleCI orb to call the CI hooks for PRs and deployments. -It's recommended that you delete your old CircleCI config and let Tool Kit -generate a new one for you that will use the Tool Kit orb for all of the -different workflows you'd typically expect in a project. - -If you have a workflow that is a little atypical then you will be free to add those in after the file has been generated (you'll want to make sure you remove the automated header comment at the top of the file so that your changes aren't overwritten in the -future.) Feel free to peruse the [source -code](https://github.com/Financial-Times/dotcom-tool-kit/tree/HEAD/orb/src) and -the -[documentation](https://circleci.com/developer/orbs/orb/financial-times/dotcom-tool-kit) -to see the inner workings of the orb. - -### Deleting n-gage - -``` -Should we uninstall obsolete n-gage and n-heroku-tools packages? -``` - -n-gage and n-heroku-tools have now been replaced by Tool Kit and shouldn't be -required any more, so we suggest you delete them from your `package.json`. - -You may opt to keep them for now if there are some esoteric tasks that -they're handling that Tool Kit doesn't support yet. - -If you're having to do this, please post in the -[#cp-platforms-team](https://financialtimes.slack.com/archives/C3TJ6KXEU) Slack -channel so we can fill in that gap! - -### Confirmation and Installation - -``` -so, we're gonna: - -install the following packages: -- ${package 1} -- ${package 2} - -uninstall the following packages: -- ${package 3} -- ${package 4} - -create a .toolkitrc.yml containing: -${config contents} - -regenerate .circleci/config.yml - -sound good? -``` - -You'll be given an opportunity to review and confirm the changes you're making -before executing them. If you do confirm them then the following will happen -(skipping the steps you've not enabled): - -1. Your `package.json` will be modified with the listed packages - installed/uninstalled -2. `npm install` will be run to update your `node_modules` and - `package-lock.json` (if applicable) with the npm changes -3. The `.toolkitrc.yml` configuration file will be written listing the plugins - you've selected -4. Deleting your old CircleCI configuration file ready to have a new one - regenerated -5. Run `npx dotcom-tool-kit --install`. This command calls an `install` method - that's defined for each hook, which will handle the logic to slot the hook - into the place it's meant to 'hook' into, e.g., installing npm hooks into the - `scripts` property in the `package.json`. (This is also where the CircleCI - config is regenerated to insert the appropriate CI hooks.) - -Sometimes the final step will fail, but this usually indicates that there are -some ambiguities in your configuration that can be clarified in the subsequent steps. - -### Task Conflicts - -``` -Hook ${hook} has multiple tasks configured for it, so an order -must be specified. Please select the 1st package to run. -- ${task 1} -- ${task 2} -- finish -``` - -In some projects you will find multiple tasks are configured to use the same -hook. A common example would be the `@dotcom-tool-kit/eslint` and -`@dotcom-tool-kit/mocha` plugins, which both define tasks that use the -`test:local` hook. - -Tool Kit does not assume the order you want those to run, as -in some cases one of the tasks might depend on the other, so you need to declare -the order explicitly. This is expanded on in its own [documentation -page](./resolving-hook-conflicts.md), but the migration tool takes you through -the process. - -If you don't want some of the tasks to be included in the hook at all, just select the -`finish` option after the rest of the tasks have been selected and the remainder -won't be run. - -### Setting Options - -``` -Please now configure the options for the ${plugin} plugin. -Set a value for '${option}' -``` - -You must set any required options for plugins to continue. The optional ones may be skipped. These options will be added to your `.toolkitrc`. In the future, we will add support for default values in the migration tool (they are already available within Tool Kit -itself,) so that for most cases you could accept default values, even for fields -that are required. - -Some plugins will have specially-made prompts to allow -setting options for more complex scenarios, such as the Heroku plugin which -prompts you to fill in scaling information for each app you have in your -project's Heroku pipeline. - -### Migrating Your Makefile - -``` -We recommend deleting your old Makefile as it will no longer be used. In the -future you can run tasks with 'npm run' instead. Make sure that you won't be -deleting any task logic that hasn't already been migrated to Tool Kit. If you -find anything that can't be handled by Tool Kit then please let the Platforms -team know. - -We've found some targets in your Makefile which could be migrated to Tool Kit: -- Your ${makefile target} target is likely handled by the ${target} hook in Tool Kit - -We don't know if these other Makefile targets can be migrated to Tool Kit. -Please check what they're doing: -- ${makefile target} -- ${makefile target} -``` - -Finally, the tool will give guidance on what the do with the `Makefile` in your -project that was integrated with n-gage. It will try and recommend equivalent -hooks based on the name of the targets within the `Makefile` (the `test:local` -hook for a `test` target, for example,) but sometimes you'll want to leave some -of the targets in at the beginning of the migration and gradually transition -everything over to Tool Kit. Regardless, this step just provides some -suggestions and the migration tool doesn't perform any actions so you'll need to -delete the `Makefile` yourself if you don't think it's needed anymore. - -Remember that – assuming you didn't delete it in the earlier step – some of the `make` -commands come from `n-gage` itself and won't necessarily be in the `Makefile`, -such as `make install`. - -You'll also want to ensure that: - -1. You have configured the same entry point in `.toolkitrc.yml` as in your `Makefile`. This must be set as an option (if it is not the default `server/app.js`). - - eg. - - ``` - nht run --script server/cluster.js - ``` - - in your `Makefile` would become - - ``` - options: - "@dotcom-tool-kit/node": - entry: server/cluster.js - ``` - - in `.toolkitrc.yml` - -2. You have moved any environment variables explicitly set in your `Makefile` to the appropriate rc file, eg. `.mocharc.js` for mocha unit tests - -## Migrating Your Heroku Pipeline - -The migration tool cannot automate reconfiguring your Heroku -pipeline to integrate with Tool Kit. You will need to ensure that: - -- Your pipeline is connected to the project's GitHub repository -- PRs create review apps -- Commits to main can be deployed from a staging app - -This might be how your pipeline was already configured, but you should check to make sure that -automatic deployments are still working. Try reconnecting to GitHub if they -are not. - -A step-by-step guide with screenshots can be found -[here](https://docs.google.com/document/d/1b7WlRfhiWlbDsSSGP3TllYaGMJbx9nCdAtcr8_OWEWM). - -## To conclude - -To ensure the migration has succeeded, you can run the following commands locally: - -`npm run build` \ -`npm start` \ -`npm test` - -On pull request, you can check that CI jobs are running as expected. diff --git a/docs/resolving-hook-conflicts.md b/docs/resolving-hook-conflicts.md deleted file mode 100644 index 42bf1f278..000000000 --- a/docs/resolving-hook-conflicts.md +++ /dev/null @@ -1,39 +0,0 @@ -# Hooks conflicts - -Tool Kit allows its plugins, and apps using it, to [configure tasks to run on hooks](../readme.md#hooks). It's possible for this configuration to conflict between plugins, in which case you'll need to resolve the conflict. - -## What causes conflicts? - -If you have Tool Kit plugins installed that configure different tasks to run on the same hook, that's a conflict. For example, both the `webpack` and `babel` plugins configure tasks to run on `build:*` hooks. When this happens, you'll get an error that looks like this: - -``` -These hooks are configured to run different tasks by multiple plugins: - -build:local: -- WebpackDevelopment by plugin @dotcom-tool-kit/webpack -- BabelDevelopment by plugin @dotcom-tool-kit/babel -``` - -You might not be using the conflicting plugins directly; they might be installed as dependencies of other plugins you're using. - -## Resolving conflicts - -The [Tool Kit configuration](../readme.md#configuration) in your app will override any configuration from plugins, which is where default hook tasks are defined. You can provide configuration in your `.toolkitrc.yml` or `package.json` `toolkit` field to specify which of the conflicting Tool Kit tasks you want to run. - -For example, if your app requires Webpack to run for `build:local` hooks, but not Babel: - -```yaml -hooks: - 'build:local': WebpackDevelopment -``` - -You can list an array of tasks, which will be run in sequence. For example to run Webpack _then_ Babel: - -```yaml -hooks: - 'build:local': - - WebpackDevelopment - - BabelDevelopment -``` - -Resolving the conflict isn't an arbitrary choice; it's entirely down to what your app requires, so make sure the resolution covers your usecases. For many standard usecases, there will be a plugin that includes a common set of plugins and hook resolutions for them. Installing a usecase plugin would let the plugin take care of resolution, so you won't need to do it manually in your app. diff --git a/docs/resolving-plugin-conflicts.md b/docs/resolving-plugin-conflicts.md new file mode 100644 index 000000000..4609b351a --- /dev/null +++ b/docs/resolving-plugin-conflicts.md @@ -0,0 +1,47 @@ +# Tool Kit plugin conflicts + +When two Tool Kit plugins try to configure the same thing, that's a conflict that needs to be resolved. Anything a plugin configures in its `.toolkitrc.yml` can be a conflict. That includes things like which commands are running which hooks and options for tasks, hooks & plugins. + +## Example of a plugin conflict + +Both the `webpack` and `babel` plugins configure tasks to run on the `build:*` commands. If you install both of them in your app, you'll get an error that looks like this: + +``` +These commands are configured to run different tasks by multiple plugins: + +build:local: +- Webpack by plugin @dotcom-tool-kit/webpack +- Babel by plugin @dotcom-tool-kit/babel +``` + +You might not be using the conflicting plugins directly; they might be installed as dependencies of other plugins you're using. + +## Resolving conflicts + +The [Tool Kit configuration](../readme.md#configuration) in your repo will override any configuration from plugins, which is treated as a default. You can provide configuration in your `.toolkitrc.yml` to specify which of the conflicting Tool Kit tasks you want to run. + +For example, if your app requires Webpack to run for `build:local` hooks, but not Babel: + +```yaml +commands: + 'build:local': Webpack +``` + +You can also list an array of tasks, which will be run in sequence. For example to run Webpack _then_ Babel: + +```yaml +commands: + 'build:local': + - Webpack + - Babel +``` + +Conflicts between options in plugins are handled in the same way, by providing an override in your `.toolkitrc.yml`. Note that this is a full override; options from plugins aren't merged with your repo's configuration, so you'll need to provide the full set of options for a plugin or task if you're overriding them. + +Resolving the conflict isn't an arbitrary choice; it's entirely down to what your app requires, so make sure the resolution covers your usecases. For many standard usecases, there will be a plugin that includes a common set of plugins and hook resolutions for them. Installing a usecase plugin would let the plugin take care of resolution, so you won't need to do it manually in your app. + +## How conflicts are handled internally + +Tool Kit loads plugins as a tree structure. When a plugin loads other plugins, we call those its **children**, and it their **parent**. The children are **sibling** plugins to each other. Tool Kit also considers your repo a plugin; it's the ultimate ancestor of all the other plugins loaded. + +Conflicts only occur between sibling or nibling (child plugins of a sibling) plugins. When children plugins conflict, their parent plugin can override the conflicting configuration to resolve the conflict. If the conflict is left unresloved, it bubbles up through all the parent plugins in this branch of the plugin tree, giving them all an opportunity to resolve it. If the conflict is never resolved by any parent plugins (including your repo), Tool Kit will exit with an error explaining the conflict and which plugins caused it. diff --git a/etc/concepts.sketch b/etc/concepts.sketch index 20de6f6f9..ca1e9b41f 100644 Binary files a/etc/concepts.sketch and b/etc/concepts.sketch differ diff --git a/etc/installing-hook.svg b/etc/installing-hook.svg index 88ceca2bd..5802e22ed 100644 --- a/etc/installing-hook.svg +++ b/etc/installing-hook.svg @@ -1,29 +1,27 @@ - - + installing-hook - Created with Sketch. - + - + - - + + - - - - + + + + - - - + + + - + \ No newline at end of file diff --git a/etc/plugin.svg b/etc/plugin.svg index a42161e4c..ddfbf2767 100644 --- a/etc/plugin.svg +++ b/etc/plugin.svg @@ -1,10 +1,8 @@ - - + plugin - Created with Sketch. - + @@ -12,34 +10,40 @@ - + - + + + + + - + - - - - + + + + - - + + - - + + - - - - + + + + + + \ No newline at end of file diff --git a/etc/running-command.svg b/etc/running-command.svg new file mode 100644 index 000000000..74a529623 --- /dev/null +++ b/etc/running-command.svg @@ -0,0 +1,35 @@ + + + running-command + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/etc/running-hook.svg b/etc/running-hook.svg deleted file mode 100644 index ad0b95dd8..000000000 --- a/etc/running-hook.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - running-hook - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/jest.config.base.js b/jest.config.base.js index 14634afd8..a69bf0446 100644 --- a/jest.config.base.js +++ b/jest.config.base.js @@ -1,12 +1,16 @@ -module.exports = { - preset: 'ts-jest', +const tsJestConfig = { + tsconfig: 'tsconfig.settings.json', + isolatedModules: true +} +module.exports.tsJestConfig = tsJestConfig + +/** @type {import('jest').Config} */ +module.exports.config = { testEnvironment: 'node', + testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/*.+(spec|test).[jt]s?(x)'], testPathIgnorePatterns: ['/node_modules/', '/.+/lib/', '/test/files'], clearMocks: true, - globals: { - 'ts-jest': { - tsconfig: 'tsconfig.settings.json', - isolatedModules: true - } + transform: { + '^.+\\.tsx?$': ['ts-jest', tsJestConfig] } } diff --git a/jest.config.js b/jest.config.js index dc31563b7..0ad64a419 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,7 @@ const base = require('./jest.config.base') +/** @type {import('jest').Config} */ module.exports = { - ...base, + ...base.config, projects: ['/core/*', '/plugins/*', '/lib/*'] } diff --git a/lib/base/package.json b/lib/base/package.json new file mode 100644 index 000000000..e276d4f3c --- /dev/null +++ b/lib/base/package.json @@ -0,0 +1,28 @@ +{ + "name": "@dotcom-tool-kit/base", + "version": "4.0.0-beta.0", + "description": "", + "main": "lib", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@dotcom-tool-kit/config": "^2.0.0-beta.0", + "@dotcom-tool-kit/conflict": "2.0.0-beta.0", + "@dotcom-tool-kit/validated": "2.0.0-beta.0", + "semver": "^7.5.4", + "winston": "^3.11.0" + }, + "devDependencies": { + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", + "winston": "^3.11.0", + "zod": "^3.22.4" + }, + "peerDependencies": { + "zod": "^3.22.4" + } +} diff --git a/lib/base/src/base.ts b/lib/base/src/base.ts new file mode 100644 index 000000000..8ca425c9a --- /dev/null +++ b/lib/base/src/base.ts @@ -0,0 +1,58 @@ +import { styles as s } from '@dotcom-tool-kit/logger' +import path from 'path' +import fs from 'fs' +import { baseSymbol, typeSymbol } from './symbols' +import { Validated, invalid, valid } from '@dotcom-tool-kit/validated' +import semver from 'semver' + +const packageJsonPath = path.resolve(__dirname, '../package.json') +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) +const version: string = packageJson.version + +export abstract class Base { + static version = version + version = version + + static get [typeSymbol](): symbol { + return baseSymbol + } + + get [typeSymbol](): symbol { + return baseSymbol + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static is(objectToCheck: any): objectToCheck is T { + return objectToCheck[typeSymbol] === this[typeSymbol] + } + + static isCompatible(objectToCheck: unknown): Validated { + if (!this.is(objectToCheck)) { + return invalid([ + `${s.plugin( + '@dotcom-tool-kit/base' + )} type symbol is missing, make sure that this object derives from the ${s.code('Task')} or ${s.code( + 'Hook' + )} class defined by the plugin` + ]) + } + + // an 'objectToCheck' from a plugin is compatible with this CLI if its + // version is semver-compatible with the @dotcom-tool-kit/base included by + // the CLI (which is what's calling this). so, prepend ^ to our version, + // and check our version satisfies that. + + // this lets e.g. a CLI that includes types@2.2.0 load any plugin + // that depends on any higher minor version of types. + const range = `^${this.version}` + if (semver.satisfies(objectToCheck.version, range)) { + return valid(objectToCheck as T) + } else { + return invalid([ + `object is from an outdated version of ${s.plugin( + '@dotcom-tool-kit/base' + )}, make sure you're using at least version ${s.heading(this.version)} of the plugin` + ]) + } + } +} diff --git a/lib/base/src/hook.ts b/lib/base/src/hook.ts new file mode 100644 index 000000000..9fb815b08 --- /dev/null +++ b/lib/base/src/hook.ts @@ -0,0 +1,69 @@ +import type { Logger } from 'winston' +import { Base } from './base' +import { hookSymbol, typeSymbol } from './symbols' +import type { z } from 'zod' +import type { Plugin } from '@dotcom-tool-kit/plugin' +import { Conflict, isConflict } from '@dotcom-tool-kit/conflict' + +export interface HookInstallation> { + options: Options + plugin: Plugin + forHook: string + hookConstructor: HookConstructor +} + +export abstract class Hook extends Base { + logger: Logger + // This field is used to collect hooks that share state when running their + // install methods. All hooks in the same group will run their install method + // one after the other, and then their commitInstall method will be run with + // the collected state. + installGroup?: string + + static get [typeSymbol](): symbol { + return hookSymbol + } + + get [typeSymbol](): symbol { + return hookSymbol + } + + static mergeChildInstallations( + plugin: Plugin, + childInstallations: (HookInstallation | Conflict)[] + ): (HookInstallation | Conflict)[] { + return [ + { + plugin, + conflicting: childInstallations.flatMap((installation) => + isConflict(installation) ? installation.conflicting : installation + ) + } + ] + } + + static overrideChildInstallations( + plugin: Plugin, + parentInstallation: HookInstallation, + _childInstallations: (HookInstallation | Conflict)[] + ): (HookInstallation | Conflict)[] { + return [parentInstallation] + } + + constructor(logger: Logger, public id: string, public options: z.output) { + super() + this.logger = logger.child({ hook: this.constructor.name }) + } + + abstract isInstalled(): Promise + abstract install(state?: State): Promise + async commitInstall(_state: State): Promise { + return + } +} + +export type HookConstructor = { + new (logger: Logger, id: string, options: z.output): Hook +} + +export type HookClass = HookConstructor & typeof Hook diff --git a/lib/base/src/index.ts b/lib/base/src/index.ts new file mode 100644 index 000000000..7bbac03f1 --- /dev/null +++ b/lib/base/src/index.ts @@ -0,0 +1,4 @@ +export * from './base' +export * from './hook' +export * from './init' +export * from './task' diff --git a/lib/base/src/init.ts b/lib/base/src/init.ts new file mode 100644 index 000000000..42b99279f --- /dev/null +++ b/lib/base/src/init.ts @@ -0,0 +1,28 @@ +import type { Logger } from 'winston' +import { initSymbol, typeSymbol } from './symbols' +import { Base } from './base' + +export abstract class Init extends Base { + logger: Logger + + constructor(logger: Logger) { + super() + this.logger = logger.child({ hook: this.constructor.name }) + } + + static get [typeSymbol](): symbol { + return initSymbol + } + + get [typeSymbol](): symbol { + return initSymbol + } + + abstract init(): Promise +} + +export type InitConstructor = { + new (logger: Logger): Init +} + +export type InitClass = InitConstructor & typeof Init diff --git a/lib/base/src/symbols.ts b/lib/base/src/symbols.ts new file mode 100644 index 000000000..f62c75247 --- /dev/null +++ b/lib/base/src/symbols.ts @@ -0,0 +1,14 @@ +// uses Symbol.for, not Symbol, so that they're compatible across different +// @dotcom-tool-kit/base instances + +// TODO these symbols say '@dotcom-tool-kit/types' because they used to live +// in that package. changing that would be a backwards compatibility nightmare + +// used as the name for the property we use to identify classes +export const typeSymbol = Symbol.for('@dotcom-tool-kit/types') + +// used to identify the Base, Task and Hook classes +export const baseSymbol = Symbol.for('@dotcom-tool-kit/types/base') +export const taskSymbol = Symbol.for('@dotcom-tool-kit/types/task') +export const hookSymbol = Symbol.for('@dotcom-tool-kit/types/hook') +export const initSymbol = Symbol.for('@dotcom-tool-kit/types/init') diff --git a/lib/base/src/task.ts b/lib/base/src/task.ts new file mode 100644 index 000000000..bbcf4f551 --- /dev/null +++ b/lib/base/src/task.ts @@ -0,0 +1,59 @@ +import type { z } from 'zod' +import { Base } from './base' +import { taskSymbol, typeSymbol } from './symbols' +import type { Logger } from 'winston' +import type { ValidConfig } from '@dotcom-tool-kit/config' +import { Plugin } from '@dotcom-tool-kit/plugin' + +type Default = T extends undefined ? D : T + +export type TaskRunContext = { + files?: string[] + config: ValidConfig +} + +export abstract class Task< + Options extends { + plugin?: z.ZodTypeAny + task?: z.ZodTypeAny + } = Record +> extends Base { + static get [typeSymbol](): symbol { + return taskSymbol + } + + get [typeSymbol](): symbol { + return taskSymbol + } + + logger: Logger + + constructor( + logger: Logger, + public id: string, + public plugin: Plugin, + public pluginOptions: z.output>>>, + public options: z.output>>> + ) { + super() + this.logger = logger.child({ task: id }) + } + + abstract run(runContext: TaskRunContext): Promise + + // not abstract for default behaviour of doing nothing + // eslint-disable-next-line @typescript-eslint/no-empty-function + async stop(): Promise {} +} + +export type TaskConstructor = { + new ( + logger: Logger, + id: string, + plugin: Plugin, + pluginOptions: Partial>, + options: Partial> + ): Task +} + +export type TaskClass = TaskConstructor & typeof Task diff --git a/lib/base/tsconfig.json b/lib/base/tsconfig.json new file mode 100644 index 000000000..5a73660b8 --- /dev/null +++ b/lib/base/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.settings.json", + "references": [ + { + "path": "../conflict" + }, + { + "path": "../logger" + }, + { + "path": "../plugin" + }, + { + "path": "../validated" + }, + { + "path": "../config" + } + ], + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + } +} diff --git a/lib/config/package.json b/lib/config/package.json new file mode 100644 index 000000000..a5b595a79 --- /dev/null +++ b/lib/config/package.json @@ -0,0 +1,18 @@ +{ + "name": "@dotcom-tool-kit/config", + "version": "2.0.0-beta.0", + "description": "", + "main": "lib", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@dotcom-tool-kit/conflict": "2.0.0-beta.0", + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", + "@dotcom-tool-kit/validated": "2.0.0-beta.0" + } +} diff --git a/lib/config/src/index.ts b/lib/config/src/index.ts new file mode 100644 index 000000000..0b790f4fa --- /dev/null +++ b/lib/config/src/index.ts @@ -0,0 +1,51 @@ +import type { Validated } from '@dotcom-tool-kit/validated' +import type { + CommandTask, + EntryPoint, + Plugin, + OptionsForPlugin, + OptionsForTask +} from '@dotcom-tool-kit/plugin' +import type { PluginOptions } from '@dotcom-tool-kit/schemas' +import type { Conflict } from '@dotcom-tool-kit/conflict' + +export interface RawConfig { + root: string + plugins: { [id: string]: Validated } + resolutionTrackers: { + resolvedPluginOptions: Set + substitutedPlugins: Set + resolvedPlugins: Set + reducedInstallationPlugins: Set + } + tasks: { [id: string]: EntryPoint | Conflict } + commandTasks: { [id: string]: CommandTask | Conflict } + pluginOptions: { [id: string]: OptionsForPlugin | Conflict | undefined } + taskOptions: { [id: string]: OptionsForTask | Conflict | undefined } + hooks: { [id: string]: EntryPoint | Conflict } + inits: EntryPoint[] + hookManagedFiles: Set +} + +export type ValidPluginsConfig = Omit & { + plugins: { [id: string]: Plugin } +} + +export type ValidOptionsForPlugin = Omit & { + options: PluginOptions[Id] +} + +export type ValidPluginOptions = { + [Id in keyof PluginOptions]: ValidOptionsForPlugin +} + +export type ValidConfig = Omit< + ValidPluginsConfig, + 'tasks' | 'commandTasks' | 'pluginOptions' | 'taskOptions' | 'hooks' +> & { + tasks: { [id: string]: EntryPoint } + commandTasks: { [id: string]: CommandTask } + pluginOptions: ValidPluginOptions + taskOptions: { [id: string]: OptionsForTask } + hooks: { [id: string]: EntryPoint } +} diff --git a/lib/config/tsconfig.json b/lib/config/tsconfig.json new file mode 100644 index 000000000..4be408154 --- /dev/null +++ b/lib/config/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.settings.json", + "references": [ + { + "path": "../plugin" + }, + { + "path": "../schemas" + } + ], + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + } +} diff --git a/lib/conflict/package.json b/lib/conflict/package.json new file mode 100644 index 000000000..f67ce11c9 --- /dev/null +++ b/lib/conflict/package.json @@ -0,0 +1,15 @@ +{ + "name": "@dotcom-tool-kit/conflict", + "version": "2.0.0-beta.0", + "description": "", + "main": "lib", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@dotcom-tool-kit/plugin": "2.0.0-beta.0" + } +} diff --git a/core/cli/src/conflict.ts b/lib/conflict/src/index.ts similarity index 70% rename from core/cli/src/conflict.ts rename to lib/conflict/src/index.ts index ccdac3d20..c32d7f489 100644 --- a/core/cli/src/conflict.ts +++ b/lib/conflict/src/index.ts @@ -1,4 +1,4 @@ -import type { Plugin } from '@dotcom-tool-kit/types' +import type { Plugin } from '@dotcom-tool-kit/plugin' export interface Conflict { plugin: Plugin @@ -9,6 +9,12 @@ export function isConflict(thing: unknown): thing is Conflict { return Boolean((thing as Conflict).conflicting) } +export function findConflictingEntries( + items: Record> +): [string, Conflict][] { + return Object.entries(items).filter((entry): entry is [string, Conflict] => isConflict(entry[1])) +} + export function findConflicts(items: (U | Conflict)[]): Conflict[] { const conflicts: Conflict[] = [] diff --git a/lib/package-json-hook/tsconfig.json b/lib/conflict/tsconfig.json similarity index 69% rename from lib/package-json-hook/tsconfig.json rename to lib/conflict/tsconfig.json index 1c87daee0..ddc6f4964 100644 --- a/lib/package-json-hook/tsconfig.json +++ b/lib/conflict/tsconfig.json @@ -1,15 +1,12 @@ { "extends": "../../tsconfig.settings.json", - "compilerOptions": { - "outDir": "lib", - "rootDir": "src" - }, "references": [ { - "path": "../types" + "path": "../plugin" } ], - "include": [ - "src/**/*" - ] + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + } } diff --git a/lib/doppler/.toolkitrc.yml b/lib/doppler/.toolkitrc.yml index 0abc882df..672166f4d 100644 --- a/lib/doppler/.toolkitrc.yml +++ b/lib/doppler/.toolkitrc.yml @@ -1,2 +1,4 @@ plugins: - '@dotcom-tool-kit/vault' + +version: 2 diff --git a/lib/doppler/jest.config.js b/lib/doppler/jest.config.js index 8ae711e3d..91d4858ef 100644 --- a/lib/doppler/jest.config.js +++ b/lib/doppler/jest.config.js @@ -1,7 +1,6 @@ const base = require('../../jest.config.base') module.exports = { - ...base, - collectCoverage: true, + ...base.config, moduleDirectories: ['node_modules'] } diff --git a/lib/doppler/package.json b/lib/doppler/package.json index ca24d53fd..8f8196da2 100644 --- a/lib/doppler/package.json +++ b/lib/doppler/package.json @@ -1,17 +1,16 @@ { "name": "@dotcom-tool-kit/doppler", - "version": "1.1.0", + "version": "2.0.0-beta.0", "description": "", "main": "lib", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/options": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", - "@dotcom-tool-kit/vault": "^3.2.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/vault": "4.0.0-beta.0", "tslib": "^2.3.1" }, "keywords": [], @@ -32,11 +31,12 @@ "extends": "../../package.json" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "spawk": "^1.8.1", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/lib/doppler/src/index.ts b/lib/doppler/src/index.ts index 25375fe62..d5855b39d 100644 --- a/lib/doppler/src/index.ts +++ b/lib/doppler/src/index.ts @@ -7,7 +7,7 @@ import { ToolKitError } from '@dotcom-tool-kit/error' import { styles, waitOnExit } from '@dotcom-tool-kit/logger' import { getOptions } from '@dotcom-tool-kit/options' import * as Vault from '@dotcom-tool-kit/vault' -import type { DopplerOptions as ConfiguredDopplerOptions } from '@dotcom-tool-kit/types/lib/schema/doppler' +import type { DopplerOptions as ConfiguredDopplerOptions } from '@dotcom-tool-kit/schemas/lib/plugins/doppler' export type Environment = 'prod' | 'ci' | 'dev' diff --git a/lib/doppler/tsconfig.json b/lib/doppler/tsconfig.json index 244163209..b90c2969e 100644 --- a/lib/doppler/tsconfig.json +++ b/lib/doppler/tsconfig.json @@ -11,10 +11,10 @@ "path": "../options" }, { - "path": "../types" + "path": "../vault" }, { - "path": "../vault" + "path": "../schemas" } ], "compilerOptions": { diff --git a/lib/error/package.json b/lib/error/package.json index cea6e54bd..533e6386b 100644 --- a/lib/error/package.json +++ b/lib/error/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/error", - "version": "3.2.0", + "version": "4.0.0-beta.0", "description": "", "main": "lib", "scripts": { @@ -26,7 +26,7 @@ "tslib": "^2.3.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/lib/error/src/index.ts b/lib/error/src/index.ts index c6b735c1a..9dce4b049 100644 --- a/lib/error/src/index.ts +++ b/lib/error/src/index.ts @@ -8,15 +8,15 @@ export interface ConflictingTask { plugin: string } -export interface HookTaskConflict { - hook: string +export interface CommandTaskConflict { + command: string conflictingTasks: ConflictingTask[] } export class ToolKitConflictError extends ToolKitError { - conflicts: HookTaskConflict[] + conflicts: CommandTaskConflict[] - constructor(message: string, conflicts: HookTaskConflict[]) { + constructor(message: string, conflicts: CommandTaskConflict[]) { super(message) this.conflicts = conflicts } diff --git a/lib/logger/package.json b/lib/logger/package.json index d08ad1acc..7230ac980 100644 --- a/lib/logger/package.json +++ b/lib/logger/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/logger", - "version": "3.4.0", + "version": "4.0.0-beta.0", "description": "", "main": "lib", "scripts": { @@ -23,7 +23,8 @@ "extends": "../../package.json" }, "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", + "@apaleslimghost/boxen": "^5.1.3", + "@dotcom-tool-kit/error": "4.0.0-beta.0", "ansi-colors": "^4.1.1", "ansi-regex": "^5.0.1", "triple-beam": "^1.3.0", @@ -35,7 +36,7 @@ "@types/triple-beam": "^1.3.2" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/lib/logger/src/format.ts b/lib/logger/src/format.ts index 060e0eff6..ad3e7b4d7 100644 --- a/lib/logger/src/format.ts +++ b/lib/logger/src/format.ts @@ -21,9 +21,12 @@ export const createFormatter = (thisLogger: winston.Logger) => } if (info.process) { labels += `[${styles.dim(info.process)}]` - } else { + } + + if (!info.process) { // simulate the newline present in a normal console.log (which we've - // removed from the Console transport) + // removed from the Console transport to support logging forked + // processes which don't necessarilly flush on a newline) message += '\n' } diff --git a/lib/logger/src/helpers.ts b/lib/logger/src/helpers.ts index d18ccafbc..feacb5833 100644 --- a/lib/logger/src/helpers.ts +++ b/lib/logger/src/helpers.ts @@ -8,16 +8,16 @@ import { HookTransport, consoleTransport } from './transports' import ansiRegex from 'ansi-regex' const ansiRegexText = ansiRegex().source -const whitespaceRegex = /\s|\n/g -const startRegex = new RegExp(`^(?:(?:${ansiRegexText})|\s|\n)+`) -const endRegex = new RegExp(`(?:(?:${ansiRegexText})|\s|\n)+$`) +const ansiOrWhitespaceRegexText = `(?:(?:${ansiRegexText})|\\n)+` +const startRegex = new RegExp(`^${ansiOrWhitespaceRegexText}`) +const endRegex = new RegExp(`${ansiOrWhitespaceRegexText}$`) // RIS escape code to effectively do a clear (^L) on the terminal. TypeScript's // watch mode does this and it's annoying to have the logs shunted around when // tracking multiple tasks. const ansiReset = /\x1Bc/g -// Trim whitespace whilst preserving ANSI escape codes +// Trim newlines whilst preserving ANSI escape codes function ansiTrim(message: string): string { let start = 0 let ansiStart = '' @@ -33,9 +33,7 @@ function ansiTrim(message: string): string { ansiEnd = endResult[0] end = -ansiEnd.length } - return ( - ansiStart.replace(whitespaceRegex, '') + message.slice(start, end) + ansiEnd.replace(whitespaceRegex, '') - ) + return ansiStart.replace('\n', '') + message.slice(start, end) + ansiEnd.replace('\n', '') } // Remove ANSI escape codes that mess with the terminal state. This selectively @@ -115,8 +113,10 @@ export function hookFork( readableObjectMode: true, transform: (message, _enc, callback) => { // add the log level and wrap the message for the winston stream to - // consume - callback(null, { level, message: cleanupLogs(message) }) + // consume. we preserve newlines here as, unlike other cases, this + // logger can be called in the middle of a line depending on when + // the stream is flushed. + callback(null, { level, message: stripAnsiReset(message) }) } }) ) diff --git a/lib/logger/src/styles.ts b/lib/logger/src/styles.ts index 4f9bdbec1..54dbd2f63 100644 --- a/lib/logger/src/styles.ts +++ b/lib/logger/src/styles.ts @@ -1,9 +1,12 @@ -import colours from 'ansi-colors' +import colours from 'chalk' +import stripAnsi from 'strip-ansi' +import boxen from '@apaleslimghost/boxen' // consistent styling use cases for terminal colours // don't use ansi-colors directly, define a style please export const styles = { - hook: colours.magenta, + hook: colours.yellow, + command: colours.magenta, task: colours.blueBright, plugin: colours.cyan, URL: colours.cyan.underline, @@ -16,9 +19,22 @@ export const styles = { dim: colours.grey, title: colours.bold.underline, taskHeader: colours.bgWhite.black, - errorHighlight: colours.red, - error: (string: string): string => `${styles.errorHighlight.bold('‼︎')} ${styles.title(string)}`, - warningHighlight: colours.yellow, - warning: (string: string): string => styles.warningHighlight.bold('⚠︎') + ' ' + string, - ruler: (): string => styles.dim('─'.repeat(process.stdout.columns / 2)) + errorHighlight: colours.bgRed.black, + error: (string: string): string => `${styles.errorHighlight(' × ')} ${styles.title(string)}`, + warningHighlight: colours.bgYellow.black, + warning: (string: string): string => styles.warningHighlight(' ! ') + ' ' + string, + infoHighlight: colours.bgBlueBright.black, + info: (string: string): string => styles.infoHighlight(' i ') + ' ' + string, + helpHighlight: colours.bgGreen.black, + help: (string: string): string => styles.helpHighlight(' ? ') + ' ' + string, + ruler: (): string => styles.dim('─'.repeat(process.stdout.columns / 2)), + box: (string: string, options: Partial) => + boxen(string, { + borderStyle: 'round', + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + ...options + }), + groupHeader: (string: string) => ` ╭─${'─'.repeat(stripAnsi(string).length)}─╮ +─┤ ${string} ├─${'─'.repeat(process.stdout.columns / 2 - stripAnsi(string).length - 6)} + ╰─${'─'.repeat(stripAnsi(string).length)}─╯` } diff --git a/lib/options/.toolkitrc.yml b/lib/options/.toolkitrc.yml index e69de29bb..d48cfb24d 100644 --- a/lib/options/.toolkitrc.yml +++ b/lib/options/.toolkitrc.yml @@ -0,0 +1,2 @@ + +version: 2 diff --git a/lib/options/package.json b/lib/options/package.json index 91233f3a9..e50b6b8fe 100644 --- a/lib/options/package.json +++ b/lib/options/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/options", - "version": "3.2.0", + "version": "4.0.0-beta.0", "description": "", "main": "lib", "scripts": { @@ -10,7 +10,7 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "tslib": "^2.3.1" }, "repository": { @@ -25,7 +25,7 @@ ".toolkitrc.yml" ], "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/lib/options/src/index.ts b/lib/options/src/index.ts index 230d1dba5..6ad4d64e1 100644 --- a/lib/options/src/index.ts +++ b/lib/options/src/index.ts @@ -1,11 +1,11 @@ -import type { Options } from '@dotcom-tool-kit/types/src/schema' +import type { PluginOptions } from '@dotcom-tool-kit/schemas' -const options: Partial = {} +const options: Partial = {} -export type OptionKey = keyof Options +export type OptionKey = keyof PluginOptions -export const getOptions = (plugin: T): Options[T] | undefined => options[plugin] +export const getOptions = (plugin: T): PluginOptions[T] | undefined => options[plugin] -export const setOptions = (plugin: T, opts: Options[T]): void => { +export const setOptions = (plugin: T, opts: PluginOptions[T]): void => { options[plugin] = opts } diff --git a/lib/options/tsconfig.json b/lib/options/tsconfig.json index d4577cf4e..3da3621a6 100644 --- a/lib/options/tsconfig.json +++ b/lib/options/tsconfig.json @@ -6,7 +6,7 @@ }, "references": [ { - "path": "../types" + "path": "../schemas" } ], "include": ["src/**/*"] diff --git a/lib/package-json-hook/src/index.ts b/lib/package-json-hook/src/index.ts deleted file mode 100644 index 4e7ade42b..000000000 --- a/lib/package-json-hook/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { PackageJsonScriptHook, PackageJsonScriptHook as PackageJsonHook } from './script-hook' -export { PackageJsonHelper } from './package-json-helper' diff --git a/lib/package-json-hook/src/package-json-helper.ts b/lib/package-json-hook/src/package-json-helper.ts deleted file mode 100644 index cc3eff4c8..000000000 --- a/lib/package-json-hook/src/package-json-helper.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Hook } from '@dotcom-tool-kit/types' -import fs from 'fs' -import get from 'lodash/get' -import mapValues from 'lodash/mapValues' -import merge from 'lodash/merge' -import update from 'lodash/update' -import path from 'path' - -interface PackageJson { - [field: string]: PackageJson | string -} - -interface PackageJsonStateValue { - hooks: string[] - trailingString: string -} - -interface PackageJsonState { - [field: string]: PackageJsonState | PackageJsonStateValue -} - -export abstract class PackageJsonHelper extends Hook { - private _packageJson?: PackageJson - abstract field: string | string[] - abstract key: string - abstract hook: string - // Allow some extra characters to be appended to the end of a hooked field. - // This is useful if you, for example, need to append the '--' argument - // delimiter to commands to allow files to be passed as additional arguments. - trailingString?: string - - installGroup = 'package-json' - - filepath = path.resolve(process.cwd(), 'package.json') - - async getPackageJson(): Promise { - if (!this._packageJson) { - const rawPackageJson = await fs.promises.readFile(this.filepath, 'utf8') - const packageJson = JSON.parse(rawPackageJson) - this._packageJson = packageJson - return packageJson - } - - return this._packageJson - } - - private get hookPath(): string[] { - return Array.isArray(this.field) ? [...this.field, this.key] : [this.field, this.key] - } - - async check(): Promise { - const packageJson = await this.getPackageJson() - return get(packageJson, this.hookPath)?.includes(this.hook) - } - - async install(state?: PackageJsonState): Promise { - state ??= {} - // prepend each hook to maintain the same order as previous implementations - update(state, this.hookPath, (hookState?: PackageJsonStateValue) => ({ - hooks: [this.hook, ...(hookState?.hooks ?? [])], - trailingString: this.trailingString - })) - return state - } - - async commitInstall(state: PackageJsonState): Promise { - const reduceHooks = (state: PackageJsonState): PackageJson => - mapValues(state, (field) => - Array.isArray(field?.hooks) - ? `dotcom-tool-kit ${field.hooks.join(' ')}${ - field.trailingString ? ' ' + field.trailingString : '' - }` - : reduceHooks(field as PackageJsonState) - ) - - const newPackageJson = merge(await this.getPackageJson(), reduceHooks(state)) - await fs.promises.writeFile(this.filepath, JSON.stringify(newPackageJson, null, 2) + '\n') - } -} diff --git a/lib/package-json-hook/src/script-hook.ts b/lib/package-json-hook/src/script-hook.ts deleted file mode 100644 index 54eca66a0..000000000 --- a/lib/package-json-hook/src/script-hook.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PackageJsonHelper } from './package-json-helper' - -export abstract class PackageJsonScriptHook extends PackageJsonHelper { - field = 'scripts' -} diff --git a/lib/package-json-hook/test/index.test.ts b/lib/package-json-hook/test/index.test.ts deleted file mode 100644 index b88ffca9e..000000000 --- a/lib/package-json-hook/test/index.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { describe, it, expect } from '@jest/globals' -import * as path from 'path' -import { promises as fs } from 'fs' -import { PackageJsonHelper } from '../src' -import winston, { Logger } from 'winston' - -const logger = (winston as unknown) as Logger - -describe('package.json hook', () => { - class TestHook extends PackageJsonHelper { - field: string | string[] = 'scripts' - key = 'test-hook' - hook = 'test:hook' - } - - const originalDir = process.cwd() - - afterEach(() => { - process.chdir(originalDir) - }) - - describe('check', () => { - it('should return true when package.json has hook call in script', async () => { - process.chdir(path.join(__dirname, 'files', 'with-hook')) - const hook = new TestHook(logger) - - expect(await hook.check()).toBeTruthy() - }) - - it('should return true when script includes other hooks', async () => { - process.chdir(path.join(__dirname, 'files', 'multiple-hooks')) - const hook = new TestHook(logger) - - expect(await hook.check()).toBeTruthy() - }) - - it(`should return false when package.json doesn't have hook call in script`, async () => { - process.chdir(path.join(__dirname, 'files', 'without-hook')) - const hook = new TestHook(logger) - - expect(await hook.check()).toBeFalsy() - }) - }) - - describe('install', () => { - it(`should add script when it doesn't exist`, async () => { - const base = path.join(__dirname, 'files', 'without-hook') - - const pkgPath = path.join(base, 'package.json') - const originalJson = await fs.readFile(pkgPath, 'utf-8') - - process.chdir(base) - - try { - const hook = new TestHook(logger) - const state = await hook.install() - await hook.commitInstall(state) - - const packageJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8')) - - expect(packageJson).toHaveProperty(['scripts', 'test-hook'], 'dotcom-tool-kit test:hook') - } finally { - await fs.writeFile(pkgPath, originalJson) - } - }) - - it(`should append trailingString field`, async () => { - const base = path.join(__dirname, 'files', 'without-hook') - - const pkgPath = path.join(base, 'package.json') - const originalJson = await fs.readFile(pkgPath, 'utf-8') - - process.chdir(base) - - try { - class TestAppendHook extends TestHook { - trailingString = '--' - } - - const hook = new TestAppendHook(logger) - const state = await hook.install() - await hook.commitInstall(state) - - const packageJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8')) - - expect(packageJson).toHaveProperty(['scripts', 'test-hook'], 'dotcom-tool-kit test:hook --') - } finally { - await fs.writeFile(pkgPath, originalJson) - } - }) - - it(`should allow nested field property`, async () => { - const base = path.join(__dirname, 'files', 'without-hook') - - const pkgPath = path.join(base, 'package.json') - const originalJson = await fs.readFile(pkgPath, 'utf-8') - - process.chdir(base) - - try { - class TestNestedHook extends TestHook { - field = ['scripts', 'nested'] - } - - const hook = new TestNestedHook(logger) - const state = await hook.install() - await hook.commitInstall(state) - - const packageJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8')) - - expect(packageJson).toHaveProperty(['scripts', 'nested', 'test-hook'], 'dotcom-tool-kit test:hook') - } finally { - await fs.writeFile(pkgPath, originalJson) - } - }) - }) -}) diff --git a/lib/plugin/package.json b/lib/plugin/package.json new file mode 100644 index 000000000..13040ac27 --- /dev/null +++ b/lib/plugin/package.json @@ -0,0 +1,13 @@ +{ + "name": "@dotcom-tool-kit/plugin", + "version": "2.0.0-beta.0", + "description": "", + "main": "lib", + "devDependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/lib/plugin/src/index.ts b/lib/plugin/src/index.ts new file mode 100644 index 000000000..9f3c55327 --- /dev/null +++ b/lib/plugin/src/index.ts @@ -0,0 +1,54 @@ +type TaskSpecWithOptions = Record> +type TaskSpec = string | TaskSpecWithOptions +type InstallsSpec = { + entryPoint: string + managesFiles?: string[] +} + +export const CURRENT_RC_FILE_VERSION = 2 + +export type RCFile = { + version?: typeof CURRENT_RC_FILE_VERSION + plugins: string[] + installs: { [id: string]: InstallsSpec } + tasks: { [id: string]: string } + commands: { [id: string]: TaskSpec | TaskSpec[] } + hooks?: { [id: string]: TaskSpec | TaskSpec[] } + options: { + plugins: { [id: string]: Record } + tasks: { [id: string]: Record } + hooks: { [id: string]: Record }[] + } + init: string[] +} + +export interface Plugin { + id: string + root: string + rcFile?: RCFile + parent?: Plugin + children?: Plugin[] +} + +export interface CommandTask { + id: string + plugin: Plugin + tasks: OptionsForTask[] +} + +export interface OptionsForPlugin { + options: Record + plugin: Plugin + forPlugin: Plugin +} + +export interface OptionsForTask { + options: Record + plugin: Plugin + task: string +} + +export interface EntryPoint { + plugin: Plugin + modulePath: string +} diff --git a/lib/plugin/tsconfig.json b/lib/plugin/tsconfig.json new file mode 100644 index 000000000..72bbe0165 --- /dev/null +++ b/lib/plugin/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + } +} diff --git a/lib/types/README.md b/lib/schemas/README.md similarity index 97% rename from lib/types/README.md rename to lib/schemas/README.md index 42ff55ddb..f184863ee 100644 --- a/lib/types/README.md +++ b/lib/schemas/README.md @@ -1,5 +1,7 @@ # Tool Kit Option Schemas +> ⚠️ this README is out of date and describes the pre-Zod schema types. update plz + ## Overview Tool Kit allows plugins to take options from a `.toolkitrc.yml` file. Each diff --git a/lib/schemas/package.json b/lib/schemas/package.json new file mode 100644 index 000000000..3ea67874c --- /dev/null +++ b/lib/schemas/package.json @@ -0,0 +1,20 @@ +{ + "name": "@dotcom-tool-kit/schemas", + "version": "2.0.0-beta.0", + "description": "", + "main": "lib", + "devDependencies": { + "prompts": "^2.4.2", + "winston": "^3.11.0", + "zod": "^3.22.4" + }, + "peerDependencies": { + "zod": "^3.22.4" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/lib/types/src/bizOps.ts b/lib/schemas/src/bizOps.ts similarity index 100% rename from lib/types/src/bizOps.ts rename to lib/schemas/src/bizOps.ts diff --git a/lib/schemas/src/hooks.ts b/lib/schemas/src/hooks.ts new file mode 100644 index 000000000..d53148a75 --- /dev/null +++ b/lib/schemas/src/hooks.ts @@ -0,0 +1,10 @@ +import { CircleCiSchema } from './hooks/circleci' +import { PackageJsonSchema } from './hooks/package-json' +import { type InferSchemaOptions } from './infer' + +export const HookSchemas = { + PackageJson: PackageJsonSchema, + CircleCi: CircleCiSchema +} + +export type HookOptions = InferSchemaOptions diff --git a/lib/schemas/src/hooks/circleci.ts b/lib/schemas/src/hooks/circleci.ts new file mode 100644 index 000000000..9dc55f547 --- /dev/null +++ b/lib/schemas/src/hooks/circleci.ts @@ -0,0 +1,69 @@ +import { z } from 'zod' + +export const CircleCiExecutor = z.object({ + name: z.string(), + image: z.string() +}) +export type CircleCiExecutor = z.infer + +export const CircleCiJob = z.object({ + name: z.string(), + command: z.string() +}) +export type CircleCiJob = z.infer + +export const CircleCiWorkflowJob = z.object({ + name: z.string(), + requires: z.array(z.string()), + splitIntoMatrix: z.boolean().optional(), + custom: z.unknown().optional() +}) +export type CircleCiWorkflowJob = z.infer + +export const CircleCiWorkflow = z.object({ + name: z.string(), + jobs: z.array(CircleCiWorkflowJob), + runOnRelease: z.boolean().optional(), + custom: z.unknown().optional() +}) +export type CircleCiWorkflow = z.infer + +export const CircleCiCustomConfig = z.record(z.unknown()) +export type CircleCiCustomConfig = z.infer + +export const CircleCiSchema = z.object({ + executors: z + .array(CircleCiExecutor) + .optional() + .describe('an array of additional CircleCI executors to output in the generated config.'), + jobs: z + .array(CircleCiJob) + .optional() + .describe( + 'an array of additional CircleCI jobs to output in the generated config. these are used for running Tool Kit commands. for running arbitrary shell commands, use `custom`.' + ), + workflows: z + .array(CircleCiWorkflow) + .optional() + .describe( + 'an array of additional CircleCI workflows to output in the generated config. these reference jobs defined in the `jobs` option.' + ), + custom: CircleCiCustomConfig.optional().describe( + 'arbitrary additional CircleCI configuration that will be merged into the Tool Kit-generated config.' + ), + disableBaseConfig: z + .boolean() + .optional() + .describe( + 'set to `true` to omit the Tool Kit CircleCI boilerplate. should be used along with `custom` to provide your own boilerplate.' + ) +}) + .describe(`This hook automatically manages \`.circleci/config.yml\` in your repo to provide configuration for CircleCI workflows to run Tool Kit commands and tasks. + +Options provided in your repository's \`.toolkitrc.yml\` for this hook are merged with any Tool Kit plugin that also provides options for the hook. + +Unless they conflict, your options are appended to options from plugins, allowing you to define custom CircleCI jobs and workflows in your repository that work alongside those from plugins.`) + +export type CircleCiOptions = z.infer + +export const Schema = CircleCiSchema diff --git a/lib/schemas/src/hooks/package-json.ts b/lib/schemas/src/hooks/package-json.ts new file mode 100644 index 000000000..a7682fbc4 --- /dev/null +++ b/lib/schemas/src/hooks/package-json.ts @@ -0,0 +1,52 @@ +import { z } from 'zod' + +const CommandListSchema = z.union([z.array(z.string()), z.string()]) +export const PackageJsonSchema = z.record( + z.record( + z.union([ + CommandListSchema, + z.object({ + commands: CommandListSchema, + trailingString: z.string() + }) + ]) + ) +) + .describe(`This hook accepts a nested object with a structure that matches the generated output in \`package.json\`. The values are used as Tool Kit command names to run. You can provide a single command or an array; multiple commands are concatenated in order. + +For more complex use cases, you can provide an object instead of a command. The object must contain keys \`commands\` (as above), and \`trailingString\` (which will be appended to the resulting \`dotcom-tool-kit\` CLI invocation). This is useful for tasks that accept a list of files after a trailing \`--\`. + +Options provided in your repository's \`.toolkitrc.yml\` for this hook are merged with any Tool Kit plugin that also provides options for the hook. + +For example, configuring this hook with the following options: + +~~~yml +options: + hooks: + - PackageJson: + scripts: + start: 'run:local' + customScript: + commands: + - custom:one + - custom:two + trailingString: '--' +~~~ + +will result in the following output in \`package.json\`: + +~~~json +{ + "scripts": { + "start": "dotcom-tool-kit run:local", + "customScript": "dotcom-tool-kit custom:one custom:two --" + } +} +~~~ + + +`) + +export type PackageJsonOptions = z.infer + +export const Schema = PackageJsonSchema diff --git a/lib/schemas/src/index.ts b/lib/schemas/src/index.ts new file mode 100644 index 000000000..360babd76 --- /dev/null +++ b/lib/schemas/src/index.ts @@ -0,0 +1,4 @@ +export * from './hooks' +export * from './plugins' +export * from './prompts' +export * from './tasks' diff --git a/lib/schemas/src/infer.ts b/lib/schemas/src/infer.ts new file mode 100644 index 000000000..dd88c3312 --- /dev/null +++ b/lib/schemas/src/infer.ts @@ -0,0 +1,6 @@ +import { type z } from 'zod' + +// Gives the TypeScript type represented by each Schema +export type InferSchemaOptions = { + [key in keyof Schemas]: Schemas[key] extends z.ZodTypeAny ? z.infer : never +} diff --git a/lib/schemas/src/plugins.ts b/lib/schemas/src/plugins.ts new file mode 100644 index 000000000..ad3f1ea26 --- /dev/null +++ b/lib/schemas/src/plugins.ts @@ -0,0 +1,41 @@ +import { z } from 'zod' + +import { CircleCISchema } from './plugins/circleci' +import { DopplerSchema } from './plugins/doppler' +import { RootSchema } from './plugins/dotcom-tool-kit' +import { HerokuSchema } from './plugins/heroku' +import { LintStagedNpmSchema } from './plugins/lint-staged-npm' +import { NextRouterSchema } from './plugins/next-router' +import { ServerlessSchema } from './plugins/serverless' +import { VaultSchema } from './plugins/vault' +import { type InferSchemaOptions } from './infer' + +// TODO:KB:20240412 remove legacyPluginOptions in a future major version +export const legacyPluginOptions: Record = { + '@dotcom-tool-kit/babel': 'Babel', + '@dotcom-tool-kit/cypress': 'Cypress', + '@dotcom-tool-kit/eslint': 'ESLint', + '@dotcom-tool-kit/jest': 'Jest', + '@dotcom-tool-kit/mocha': 'Mocha', + '@dotcom-tool-kit/n-test': 'NTest', + '@dotcom-tool-kit/node': 'Node', + '@dotcom-tool-kit/nodemon': 'Nodemon', + '@dotcom-tool-kit/pa11y': 'Pa11y', + '@dotcom-tool-kit/prettier': 'Prettier', + '@dotcom-tool-kit/typescript': 'TypeScript', + '@dotcom-tool-kit/upload-assets-to-s3': 'UploadAssetsToS3', + '@dotcom-tool-kit/webpack': 'Webpack' +} + +export const PluginSchemas = { + 'app root': RootSchema, + '@dotcom-tool-kit/circleci': CircleCISchema, + '@dotcom-tool-kit/doppler': DopplerSchema, + '@dotcom-tool-kit/heroku': HerokuSchema, + '@dotcom-tool-kit/lint-staged-npm': LintStagedNpmSchema, + '@dotcom-tool-kit/next-router': NextRouterSchema, + '@dotcom-tool-kit/serverless': ServerlessSchema, + '@dotcom-tool-kit/vault': VaultSchema +} + +export type PluginOptions = InferSchemaOptions diff --git a/lib/schemas/src/plugins/circleci.ts b/lib/schemas/src/plugins/circleci.ts new file mode 100644 index 000000000..2b572f49f --- /dev/null +++ b/lib/schemas/src/plugins/circleci.ts @@ -0,0 +1,14 @@ +import { z } from 'zod' + +export const CircleCISchema = z.object({ + cimgNodeVersions: z + .string() + .array() + .default(['18.19-browsers']) + .describe( + 'list of CircleCI [Node.js image versions](https://circleci.com/developer/images/image/cimg/node) to use. if more than one is provided, a [matrix build](https://circleci.com/docs/using-matrix-jobs/) will be generated in your CircleCI config.' + ) +}) +export type CircleCIOptions = z.infer + +export const Schema = CircleCISchema diff --git a/lib/types/src/schema/doppler.ts b/lib/schemas/src/plugins/doppler.ts similarity index 100% rename from lib/types/src/schema/doppler.ts rename to lib/schemas/src/plugins/doppler.ts diff --git a/lib/types/src/schema/dotcom-tool-kit.ts b/lib/schemas/src/plugins/dotcom-tool-kit.ts similarity index 100% rename from lib/types/src/schema/dotcom-tool-kit.ts rename to lib/schemas/src/plugins/dotcom-tool-kit.ts diff --git a/lib/schemas/src/plugins/heroku.ts b/lib/schemas/src/plugins/heroku.ts new file mode 100644 index 000000000..fdcd995eb --- /dev/null +++ b/lib/schemas/src/plugins/heroku.ts @@ -0,0 +1,23 @@ +import { PromptGenerators } from '../prompts' +import { z } from 'zod' + +export const HerokuSchema = z.object({ + pipeline: z + .string() + .describe( + "the ID of your app's Heroku pipeline. this can be found at https://dashboard.heroku.com/pipelines/[PIPELINE_ID]" + ), + systemCode: z + .string() + .describe( + "your app's Biz Ops system code. this can be found at https://biz-ops.in.ft.com/System/[SYSTEM_CODE]" + ) +}) + +export type HerokuOptions = z.infer + +export const Schema = HerokuSchema +export const generators: PromptGenerators = { + pipeline: async (logger, prompt, onCancel, bizOpsSystem) => bizOpsSystem?.herokuApps[0]?.pipelineName, + systemCode: async (logger, prompt, onCancel, bizOpsSystem) => bizOpsSystem?.code +} diff --git a/lib/types/src/schema/lint-staged-npm.ts b/lib/schemas/src/plugins/lint-staged-npm.ts similarity index 68% rename from lib/types/src/schema/lint-staged-npm.ts rename to lib/schemas/src/plugins/lint-staged-npm.ts index bc076c2dc..8f22f9701 100644 --- a/lib/types/src/schema/lint-staged-npm.ts +++ b/lib/schemas/src/plugins/lint-staged-npm.ts @@ -1,8 +1,8 @@ import { z } from 'zod' export const LintStagedNpmSchema = z.object({ - testGlob: z.string().optional(), - formatGlob: z.string().optional() + testGlob: z.string().default('**/*.js'), + formatGlob: z.string().default('**/*.js') }) export type LintStagedNpmOptions = z.infer diff --git a/lib/types/src/schema/next-router.ts b/lib/schemas/src/plugins/next-router.ts similarity index 54% rename from lib/types/src/schema/next-router.ts rename to lib/schemas/src/plugins/next-router.ts index 346810aee..2a7136484 100644 --- a/lib/types/src/schema/next-router.ts +++ b/lib/schemas/src/plugins/next-router.ts @@ -4,9 +4,10 @@ export const NextRouterSchema = z.object({ appName: z .string() .describe( - 'This needs to be same as the name your app is registered under in next-service-registry. This is usually – but not always – your system code.' + "The system's `name` field as it appears in [next-service-registry](https://next-registry.ft.com/v2). **This is often different to its Biz Ops system code**, so be sure to check." ) }) + export type NextRouterOptions = z.infer export const Schema = NextRouterSchema diff --git a/lib/types/src/schema/serverless.ts b/lib/schemas/src/plugins/serverless.ts similarity index 52% rename from lib/types/src/schema/serverless.ts rename to lib/schemas/src/plugins/serverless.ts index 5086d2165..4861c3dda 100644 --- a/lib/types/src/schema/serverless.ts +++ b/lib/schemas/src/plugins/serverless.ts @@ -1,15 +1,25 @@ -import { PromptGenerators } from '../schema' +import { PromptGenerators } from '../prompts' import { z } from 'zod' export const ServerlessSchema = z.object({ - awsAccountId: z.string(), - systemCode: z.string(), - regions: z.array(z.string()).default(['eu-west-1']), - configPath: z.string().optional(), - useVault: z.boolean().default(true), - ports: z.number().array().default([3001, 3002, 3003]), - buildNumVariable: z.string().default('CIRCLE_BUILD_NUM') + awsAccountId: z + .string() + .describe( + 'the ID of the AWS account you wish to deploy to (account IDs can be found at the [FT login page](https://awslogin.in.ft.com/))' + ), + systemCode: z.string().describe('the system code for your app'), + regions: z + .array(z.string()) + .default(['eu-west-1']) + .describe('an array of AWS regions you want to deploy to'), + configPath: z + .string() + .optional() + .describe( + 'path to your serverless config file. If this is not provided, Serverless defaults to `./serverless.yml` but [other config fomats are accepted](https://www.serverless.com/framework/docs/providers/aws/guide/intro#alternative-configuration-format)' + ) }) + export type ServerlessOptions = z.infer export const Schema = ServerlessSchema diff --git a/lib/types/src/schema/vault.ts b/lib/schemas/src/plugins/vault.ts similarity index 100% rename from lib/types/src/schema/vault.ts rename to lib/schemas/src/plugins/vault.ts diff --git a/lib/schemas/src/prompts.ts b/lib/schemas/src/prompts.ts new file mode 100644 index 000000000..50a339b08 --- /dev/null +++ b/lib/schemas/src/prompts.ts @@ -0,0 +1,33 @@ +import type prompts from 'prompts' +import type { Logger } from 'winston' +import type { z } from 'zod' + +import type { BizOpsSystem } from './bizOps' + +/** + * A function that should use the `prompt` parameter passed to build a more + * complex option structure, like a nested object, from user input. Returning + * an undefined value will cause the program to fall back to the default prompt + * interface. + * @param onCancel - pass this to `prompt`'s options so that a user + * interrupting the prompt can be handled properly + */ +export type SchemaPromptGenerator = ( + logger: Logger, + prompt: typeof prompts, + onCancel: () => void, + // HACK:20231209:IM add bizOpsSystem as optional parameter to maintain + // backwards compatibility + bizOpsSystem?: BizOpsSystem +) => Promise +// This type defines an interface you can use to export prompt generators. The +// `T` type parameter should be the type of your `Schema` object, and it will +// be mapped into a partial object of `SchemaPromptGenerator` functions with +// all their return types set to the output type of each option schema. +export type PromptGenerators = T extends z.ZodObject + ? { + [option in keyof Shape as Shape[option] extends z.ZodType + ? option + : never]?: Shape[option] extends z.ZodType ? SchemaPromptGenerator> : never + } + : never diff --git a/lib/schemas/src/tasks.ts b/lib/schemas/src/tasks.ts new file mode 100644 index 000000000..2f1c41bfa --- /dev/null +++ b/lib/schemas/src/tasks.ts @@ -0,0 +1,48 @@ +import { type InferSchemaOptions } from './infer' +import { BabelSchema } from './tasks/babel' +import { ESLintSchema } from './tasks/eslint' +import { JestSchema } from './tasks/jest' +import { MochaSchema } from './tasks/mocha' +import { NodeSchema } from './tasks/node' +import { NodemonSchema } from './tasks/nodemon' +import { Pa11ySchema } from './tasks/pa11y' +import { PrettierSchema } from './tasks/prettier' +import { TypeScriptSchema } from './tasks/typescript' +import { UploadAssetsToS3Schema } from './tasks/upload-assets-to-s3' +import { WebpackSchema } from './tasks/webpack' +import { SmokeTestSchema } from './tasks/n-test' +import { CypressSchema } from './tasks/cypress' +import { HerokuProductionSchema } from './tasks/heroku-production' +import { ServerlessRunSchema } from './tasks/serverless-run' +import { z } from 'zod' +import { ParallelSchema } from './tasks/parallel' + +export const TaskSchemas = { + Babel: BabelSchema, + Cypress: CypressSchema, + Eslint: ESLintSchema, + HerokuProduction: HerokuProductionSchema, + HerokuReview: z.object({}).describe('Create and deploy a Heroku review app.'), + HerokuStaging: z.object({}).describe('Deploy to the Heroku staging app.'), + HerokuTeardown: z.object({}).describe("Scale down the Heroku staging app once it's no longer needed."), + Jest: JestSchema, + LintStaged: z.object({}).describe('Run `lint-staged` in your repo, for use with git hooks.'), + Mocha: MochaSchema, + Node: NodeSchema, + Nodemon: NodemonSchema, + NpmPrune: z.object({}).describe('Prune development npm dependencies.'), + NpmPublish: z.object({}).describe('Publish package to the npm registry.'), + NTest: SmokeTestSchema, + Pa11y: Pa11ySchema, + Parallel: ParallelSchema, + Prettier: PrettierSchema, + ServerlessDeploy: z.object({}).describe('Deploy a serverless function'), + ServerlessProvision: z.object({}).describe('Provision a review serverless function'), + ServerlessRun: ServerlessRunSchema, + ServerlessTeardown: z.object({}).describe('Tear down existing serverless functions'), + TypeScript: TypeScriptSchema, + UploadAssetsToS3: UploadAssetsToS3Schema, + Webpack: WebpackSchema +} + +export type TaskOptions = InferSchemaOptions diff --git a/lib/schemas/src/tasks/babel.ts b/lib/schemas/src/tasks/babel.ts new file mode 100644 index 000000000..b0b75d676 --- /dev/null +++ b/lib/schemas/src/tasks/babel.ts @@ -0,0 +1,19 @@ +import { z } from 'zod' + +export const BabelSchema = z + .object({ + files: z.string().default('src/**/*.js').describe('a glob pattern of files to build in your repo'), + outputPath: z.string().default('lib').describe('folder to output built files into'), + configFile: z + .string() + .optional() + .describe('path to the Babel [config file](https://babeljs.io/docs/configuration) to use'), + envName: z + .union([z.literal('production'), z.literal('development')]) + .describe('the Babel [environment](https://babeljs.io/docs/options#env) to use') + }) + .describe('Compile files with Babel') + +export type BabelOptions = z.infer + +export const Schema = BabelSchema diff --git a/lib/schemas/src/tasks/cypress.ts b/lib/schemas/src/tasks/cypress.ts new file mode 100644 index 000000000..2984510ea --- /dev/null +++ b/lib/schemas/src/tasks/cypress.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' + +export const CypressSchema = z + .object({ + url: z + .string() + .optional() + .describe( + 'URL to run Cypress against. If running in an environment such as a review or staging app build that has Tool Kit state with a URL for an app to run against, that will override this option.' + ) + }) + .describe('Run Cypress end-to-end tests') + +export type CypressOptions = z.infer + +export const Schema = CypressSchema diff --git a/lib/schemas/src/tasks/eslint.ts b/lib/schemas/src/tasks/eslint.ts new file mode 100644 index 000000000..4ec1d894c --- /dev/null +++ b/lib/schemas/src/tasks/eslint.ts @@ -0,0 +1,24 @@ +import { z } from 'zod' + +export const ESLintSchema = z + .object({ + configPath: z + .string() + .optional() + .describe( + 'Path to the [ESLint config file](https://eslint.org/docs/v8.x/use/configure/configuration-files) to use.' + ), + files: z + .string() + .array() + .or(z.string()) + .default(['**/*.js']) + .describe( + 'The glob patterns for lint target files. This can either be a string or an array of strings.' + ) + }) + .describe('Runs `eslint` to lint and format target files.') + +export type ESLintOptions = z.infer + +export const Schema = ESLintSchema diff --git a/lib/types/src/schema/heroku.ts b/lib/schemas/src/tasks/heroku-production.ts similarity index 78% rename from lib/types/src/schema/heroku.ts rename to lib/schemas/src/tasks/heroku-production.ts index 7b6e09a02..1f6f0913a 100644 --- a/lib/types/src/schema/heroku.ts +++ b/lib/schemas/src/tasks/heroku-production.ts @@ -1,21 +1,8 @@ -import { SchemaPromptGenerator, PromptGenerators } from '../schema' - -import { waitOnExit } from '@dotcom-tool-kit/logger' - -import { spawn } from 'node:child_process' - -import type { Logger } from 'winston' import { z } from 'zod' - -export const HerokuScalingSchema = z.record( - z.record( - z.object({ - size: z.string(), - quantity: z.number() - }) - ) -) -export type HerokuScaling = z.infer +import { PromptGenerators, SchemaPromptGenerator } from '../prompts' +import type { Logger } from 'winston' +import { spawn } from 'node:child_process' +import { waitOnExit } from '@dotcom-tool-kit/logger' const HerokuDynoList = z.array( z.object({ @@ -24,6 +11,7 @@ const HerokuDynoList = z.array( type: z.string() }) ) + type HerokuDynoList = z.infer const getDynos = async (logger: Logger, appName: string): Promise => { @@ -127,16 +115,48 @@ const scaling: SchemaPromptGenerator = async (logger, prompt, onC return scaling } -export const HerokuSchema = z.object({ - pipeline: z.string().describe('this can be found at https://dashboard.heroku.com/pipelines/[APP_ID]'), - systemCode: z.string().describe('this can be found at https://biz-ops.in.ft.com/System/[APP_NAME]'), +export const HerokuScalingSchema = z.record( + z.record( + z.object({ + size: z.string(), + quantity: z.number() + }) + ) +) + +export type HerokuScaling = z.infer + +export const HerokuProductionSchema = z.object({ scaling: HerokuScalingSchema -}) -export type HerokuOptions = z.infer +}).describe(`Promote the Heroku staging app to production. + +### Task options + +\`scaling\`: an object with scaling configuration for each app and dyno. The first-level keys are the names of your production apps, and the second level keys are names of the dynos within each app (this should usually at least include \`web\`). + +#### Scaling configuration + +| Property | Description | Type | +|-|-|-| +| \`size\` | the [Dyno type](https://devcenter.heroku.com/articles/dyno-types) for this dyno, e.g. \`standard-1x\`. apps in the FT Heroku account can only use [professional tier dynos](https://devcenter.heroku.com/articles/dyno-types#dyno-tiers-and-mixing-dyno-types). | \`string\` | +| \`quantity\` | how many of this dyno to use | \`number\` | + +#### Example + +~~~yml +options: + tasks: + HerokuProduction: + scaling: + ft-next-static-eu: + web: + size: standard-1x + quantity: 1 +~~~ + + +`) -export const Schema = HerokuSchema -export const generators: PromptGenerators = { - pipeline: async (logger, prompt, onCancel, bizOpsSystem) => bizOpsSystem?.herokuApps[0]?.pipelineName, - systemCode: async (logger, prompt, onCancel, bizOpsSystem) => bizOpsSystem?.code, +export const generators: PromptGenerators = { scaling } diff --git a/lib/schemas/src/tasks/jest.ts b/lib/schemas/src/tasks/jest.ts new file mode 100644 index 000000000..ed4c9d1d7 --- /dev/null +++ b/lib/schemas/src/tasks/jest.ts @@ -0,0 +1,20 @@ +import { z } from 'zod' + +export const JestSchema = z + .object({ + configPath: z + .string() + .optional() + .describe( + "Path to the [Jest config file](https://jestjs.io/docs/27.x/configuration). Use Jest's own [config resolution](https://jestjs.io/docs/configuration/) by default." + ), + ci: z + .literal(true) + .optional() + .describe('Whether to run Jest in [CI mode](https://jestjs.io/docs/cli#--ci).') + }) + .describe('Runs `jest` to execute tests.') + +export type JestOptions = z.infer + +export const Schema = JestSchema diff --git a/lib/schemas/src/tasks/mocha.ts b/lib/schemas/src/tasks/mocha.ts new file mode 100644 index 000000000..334399c7d --- /dev/null +++ b/lib/schemas/src/tasks/mocha.ts @@ -0,0 +1,17 @@ +import { z } from 'zod' + +export const MochaSchema = z + .object({ + files: z.string().default('test/**/*.js').describe('A file path glob to Mocha tests.'), + configPath: z + .string() + .optional() + .describe( + "Path to the [Mocha config file](https://mochajs.org/#configuring-mocha-nodejs). Uses Mocha's own [config resolution](https://mochajs.org/#priorities) by default." + ) + }) + .describe('Runs `mocha` to execute tests.') + +export type MochaOptions = z.infer + +export const Schema = MochaSchema diff --git a/lib/schemas/src/tasks/n-test.ts b/lib/schemas/src/tasks/n-test.ts new file mode 100644 index 000000000..ca69d4ab1 --- /dev/null +++ b/lib/schemas/src/tasks/n-test.ts @@ -0,0 +1,20 @@ +import { z } from 'zod' + +export const SmokeTestSchema = z + .object({ + browsers: z.string().array().optional().describe('Selenium browsers to run the test against'), + host: z + .string() + .optional() + .describe( + 'Set the hostname to use for all tests. If running in an environment such as a review or staging app build that has Tool Kit state with a URL for an app to run against, that will override this option.' + ), + config: z.string().optional().describe('Path to config file used to test'), + interactive: z.boolean().optional().describe('Interactively choose which tests to run'), + header: z.record(z.string()).optional().describe('Request headers to be sent with every request') + }) + .describe('Run [n-test](https://github.com/financial-times/n-test) smoke tests against your application.') + +export type SmokeTestOptions = z.infer + +export const Schema = SmokeTestSchema diff --git a/lib/schemas/src/tasks/node.ts b/lib/schemas/src/tasks/node.ts new file mode 100644 index 000000000..993fec9b3 --- /dev/null +++ b/lib/schemas/src/tasks/node.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' + +export const NodeSchema = z + .object({ + entry: z.string().default('./server/app.js').describe('path to the node application'), + args: z.string().array().optional().describe('additional arguments to pass to your application'), + useDoppler: z + .boolean() + .default(true) + .describe('whether to run the application with environment variables from Doppler'), + ports: z + .union([z.number().array(), z.literal(false)]) + .default([3001, 3002, 3003]) + .describe( + "ports to try to bind to for this application. set to `false` for an entry point that wouldn't bind to a port, such as a worker process or one-off script." + ) + }) + .describe('Run a Node.js application for local development.') + +export type NodeOptions = z.infer + +export const Schema = NodeSchema diff --git a/lib/schemas/src/tasks/nodemon.ts b/lib/schemas/src/tasks/nodemon.ts new file mode 100644 index 000000000..940e5e7bb --- /dev/null +++ b/lib/schemas/src/tasks/nodemon.ts @@ -0,0 +1,27 @@ +import { z } from 'zod' + +export const NodemonSchema = z + .object({ + entry: z.string().default('./server/app.js').describe('path to the node application'), + configPath: z + .string() + .optional() + .describe( + "path to a Nodemon config file. defaults to Nodemon's [automatic config resolution](https://github.com/remy/nodemon#config-files)." + ), + useDoppler: z + .boolean() + .default(true) + .describe('whether to run the application with environment variables from Doppler'), + ports: z + .union([z.number().array(), z.literal(false)]) + .default([3001, 3002, 3003]) + .describe( + "ports to try to bind to for this application. set to `false` for an entry point that wouldn't bind to a port, such as a worker process or one-off script." + ) + }) + .describe('Run an application with `nodemon` for local development.') + +export type NodemonOptions = z.infer + +export const Schema = NodemonSchema diff --git a/lib/schemas/src/tasks/pa11y.ts b/lib/schemas/src/tasks/pa11y.ts new file mode 100644 index 000000000..261d3f552 --- /dev/null +++ b/lib/schemas/src/tasks/pa11y.ts @@ -0,0 +1,11 @@ +import { z } from 'zod' + +export const Pa11ySchema = z + .object({ + configFile: z.string().optional().describe('Path to the config file') + }) + .describe('runs `pa11y-ci` to execute Pa11y tests') + +export type Pa11yOptions = z.infer + +export const Schema = Pa11ySchema diff --git a/lib/schemas/src/tasks/parallel.ts b/lib/schemas/src/tasks/parallel.ts new file mode 100644 index 000000000..392db191f --- /dev/null +++ b/lib/schemas/src/tasks/parallel.ts @@ -0,0 +1,29 @@ +import { z } from 'zod' + +export const ParallelSchema = z.object({ + tasks: z.array(z.record(z.record(z.unknown()))) +}).describe(`Run Tool Kit tasks in parallel + +### Task options + +\`tasks\`: an array listing the tasks to run in parallel, and the options to run each task with. Each element in the array is an object with a single key and value; the key is the name of the task to run, and the value is the options object for that task. Other tasks' options are documented in their plugin's readme. + +#### Example + +~~~yaml +commands: + run:local: + - Parallel: + tasks: + - Node: + entry: server/index.js + - Webpack: + watch: true +~~~ + + +`) + +export type ParallelOptions = z.infer + +export const Schema = ParallelSchema diff --git a/lib/schemas/src/tasks/prettier.ts b/lib/schemas/src/tasks/prettier.ts new file mode 100644 index 000000000..7ca2af484 --- /dev/null +++ b/lib/schemas/src/tasks/prettier.ts @@ -0,0 +1,26 @@ +import { z } from 'zod' + +export const PrettierSchema = z + .object({ + files: z + .string() + .array() + .or(z.string()) + .default(['**/*.{js,jsx,ts,tsx}']) + .describe('glob pattern of files to run Prettier on.'), + configFile: z + .string() + .optional() + .describe( + "path to a Prettier config file to use. Uses Prettier's built-in [config resolution](https://prettier.io/docs/en/configuration.html) by default." + ), + ignoreFile: z + .string() + .default('.prettierignore') + .describe('path to a Prettier [ignore file](https://prettier.io/docs/en/ignore).') + }) + .describe('Format files with `prettier`.') + +export type PrettierOptions = z.infer + +export const Schema = PrettierSchema diff --git a/lib/schemas/src/tasks/serverless-run.ts b/lib/schemas/src/tasks/serverless-run.ts new file mode 100644 index 000000000..965fead31 --- /dev/null +++ b/lib/schemas/src/tasks/serverless-run.ts @@ -0,0 +1,19 @@ +import { z } from 'zod' + +export const ServerlessRunSchema = z + .object({ + ports: z + .number() + .array() + .default([3001, 3002, 3003]) + .describe('ports to try to bind to for this application'), + useDoppler: z + .boolean() + .default(true) + .describe('run the application with environment variables from Doppler') + }) + .describe('Run serverless functions locally') + +export type ServerlessRunOptions = z.infer + +export const Schema = ServerlessRunSchema diff --git a/lib/schemas/src/tasks/typescript.ts b/lib/schemas/src/tasks/typescript.ts new file mode 100644 index 000000000..4dde98c62 --- /dev/null +++ b/lib/schemas/src/tasks/typescript.ts @@ -0,0 +1,29 @@ +import { z } from 'zod' + +export const TypeScriptSchema = z + .object({ + configPath: z + .string() + .optional() + .describe( + "to the [TypeScript config file](https://www.typescriptlang.org/tsconfig). Uses TypeScript's own [tsconfig.json resolution](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#using-tsconfigjson-or-jsconfigjson) by default" + ), + build: z + .literal(true) + .optional() + .describe( + 'Run Typescript in [build mode](https://www.typescriptlang.org/docs/handbook/project-references.html#build-mode-for-typescript).' + ), + watch: z.literal(true).optional().describe('Run Typescript in watch mode.'), + noEmit: z + .literal(true) + .optional() + .describe( + 'Run Typescript with `--noEmit`, for checking your types without outputting compiled Javascript.' + ) + }) + .describe('Compile code with `tsc`.') + +export type TypeScriptOptions = z.infer + +export const Schema = TypeScriptSchema diff --git a/lib/schemas/src/tasks/upload-assets-to-s3.ts b/lib/schemas/src/tasks/upload-assets-to-s3.ts new file mode 100644 index 000000000..40e894f24 --- /dev/null +++ b/lib/schemas/src/tasks/upload-assets-to-s3.ts @@ -0,0 +1,58 @@ +import { z } from 'zod' + +export const UploadAssetsToS3Schema = z + .object({ + accessKeyIdEnvVar: z + .string() + .default('AWS_ACCESS_HASHED_ASSETS') + .describe( + "variable name of the project's aws access key id. If uploading to multiple buckets the same credentials will need to work for all" + ), + secretAccessKeyEnvVar: z + .string() + .default('AWS_SECRET_HASHED_ASSETS') + .describe("variable name of the project's aws secret access key"), + directory: z + .string() + .default('public') + .describe('the folder in the project whose contents will be uploaded to S3'), + reviewBucket: z + .string() + .array() + .default(['ft-next-hashed-assets-preview']) + .describe('the development or test S3 bucket'), + prodBucket: z + .string() + .array() + .default(['ft-next-hashed-assets-prod']) + .describe( + "production S3 bucket(s). The same files will be uploaded to each. **Note**: most Customer Products buckets that have a `prod` and `prod-us` version are already configured in AWS to replicate file changes from one to the other so you don't need to specify both here. Also, if multiple buckets are specified the same credentials will need to be valid for both for the upload to be successful." + ), + region: z + .string() + .default('eu-west-1') + .describe( + 'the AWS region your buckets are stored in (let the Platforms team know if you need to upload to multiple buckets in multiple regions).' + ), + destination: z + .string() + .default('hashed-assets/page-kit') + .describe( + "the destination folder for uploaded assets. Set to `''` to upload assets to the top level of the bucket" + ), + extensions: z + .string() + .default('js,css,map,gz,br,png,jpg,jpeg,gif,webp,svg,ico,json') + .describe('file extensions to be uploaded to S3'), + cacheControl: z + .string() + .default('public, max-age=31536000, stale-while-revalidate=60, stale-if-error=3600') + .describe( + 'header that controls how long your files stay in a CloudFront cache before CloudFront forwards another request to your origin' + ) + }) + .describe('Upload files to an AWS S3 bucket.') + +export type UploadAssetsToS3Options = z.infer + +export const Schema = UploadAssetsToS3Schema diff --git a/lib/schemas/src/tasks/webpack.ts b/lib/schemas/src/tasks/webpack.ts new file mode 100644 index 000000000..6b5de4e6c --- /dev/null +++ b/lib/schemas/src/tasks/webpack.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' + +export const WebpackSchema = z + .object({ + configPath: z + .string() + .optional() + .describe('path to a Webpack config file. Webpack will default to `webpack.config.js`.'), + envName: z + .union([z.literal('production'), z.literal('development')]) + .describe("set Webpack's [mode](https://webpack.js.org/configuration/mode/)."), + watch: z.boolean().optional().describe('run Webpack in watch mode') + }) + .describe('Bundle code with `webpack`.') + +export type WebpackOptions = z.infer + +export const Schema = WebpackSchema diff --git a/lib/schemas/tsconfig.json b/lib/schemas/tsconfig.json new file mode 100644 index 000000000..72bbe0165 --- /dev/null +++ b/lib/schemas/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + } +} diff --git a/lib/state/package.json b/lib/state/package.json index 77f2a99cb..799a98e7b 100644 --- a/lib/state/package.json +++ b/lib/state/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/state", - "version": "3.3.0", + "version": "4.0.0-beta.0", "description": "", "main": "lib", "scripts": { @@ -27,7 +27,7 @@ "tslib": "^2.3.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/lib/state/src/index.ts b/lib/state/src/index.ts index daebf5baf..61c568a0c 100644 --- a/lib/state/src/index.ts +++ b/lib/state/src/index.ts @@ -4,11 +4,14 @@ import * as fs from 'fs' const target = process.env.INIT_CWD || process.cwd() const stateDir = target ? path.join(target, '.toolkitstate') : '.toolkitstate' +type InstallState = Record + interface CIState { repo: string branch: string version: string tag: string + buildNumber: string } export interface ReviewState { @@ -39,6 +42,7 @@ export interface State { staging: StagingState production: ProductionState local: LocalState + install: InstallState } export function readState(stage: T): State[T] | null { diff --git a/lib/types/CHANGELOG.md b/lib/types/CHANGELOG.md deleted file mode 100644 index 4ce737819..000000000 --- a/lib/types/CHANGELOG.md +++ /dev/null @@ -1,307 +0,0 @@ -# Changelog - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/logger bumped from ^2.1.0 to ^2.1.1 - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/logger bumped from ^2.1.1 to ^2.2.0 - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/logger bumped from ^3.2.0 to ^3.3.0 - -## [3.6.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v3.5.0...types-v3.6.0) (2024-01-11) - - -### Features - -* add support for Node v20 ([759ac10](https://github.com/Financial-Times/dotcom-tool-kit/commit/759ac10e309885e99f54ae431c301c32ee04f972)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/error bumped from ^3.1.0 to ^3.2.0 - * @dotcom-tool-kit/logger bumped from ^3.3.1 to ^3.4.0 - -## [3.5.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v3.4.1...types-v3.5.0) (2023-12-18) - - -### Features - -* **create:** add suggestions to all required options ([2569e3f](https://github.com/Financial-Times/dotcom-tool-kit/commit/2569e3fdefc193298306844622f6238e74826084)) -* **create:** infer Heroku options based on Biz Ops and Heroku API ([8aa02b6](https://github.com/Financial-Times/dotcom-tool-kit/commit/8aa02b6916592abcdfbf3afa8c6c52e43dff83c5)) -* **create:** infer the rest of the Heroku options from Biz Ops data ([e3c626e](https://github.com/Financial-Times/dotcom-tool-kit/commit/e3c626ebfda662845d72c7130d197414fa922a91)) - - -### Bug Fixes - -* **create:** default all confirmation prompts to the happy path ([8dca215](https://github.com/Financial-Times/dotcom-tool-kit/commit/8dca2157ed42a39e36fb862641a7c6758c64db71)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/logger bumped from ^3.3.0 to ^3.3.1 - -## [3.4.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v3.3.1...types-v3.4.0) (2023-09-19) - - -### Features - -* **doppler:** add library to get secrets from doppler ([ce51a90](https://github.com/Financial-Times/dotcom-tool-kit/commit/ce51a904cdaffdf8e490e9cc09ad4a2ac14f255b)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/logger bumped from ^3.1.1 to ^3.2.0 - -## [3.3.1](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v3.3.0...types-v3.3.1) (2023-07-04) - - -### Bug Fixes - -* install Reliability Kit ESLint config and fix errors found ([35a6f77](https://github.com/Financial-Times/dotcom-tool-kit/commit/35a6f7754c33f58789b201594ed5d1000e029f1c)) - -## [3.3.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v3.2.0...types-v3.3.0) (2023-06-14) - - -### Features - -* disable Node 18's native fetch across all plugins ([ba10618](https://github.com/Financial-Times/dotcom-tool-kit/commit/ba10618f9eb861b8499255fcdb297502e7c42bdf)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/logger bumped from ^3.1.0 to ^3.1.1 - -## [3.2.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v3.1.0...types-v3.2.0) (2023-05-30) - - -### Features - -* **circleci:** add default value for CircleCI's node version option ([ab5ce94](https://github.com/Financial-Times/dotcom-tool-kit/commit/ab5ce94441983693d3849ee42cf0f1c30fcff67e)) -* **circleci:** add support for multiple Node versions ([10b15f4](https://github.com/Financial-Times/dotcom-tool-kit/commit/10b15f42f603c232293e15d05d4a062d7f855dbb)) - -## [3.1.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v3.0.0...types-v3.1.0) (2023-04-28) - - -### Features - -* **node:** disable native fetch in forked node processes ([73234e4](https://github.com/Financial-Times/dotcom-tool-kit/commit/73234e43d52a7dd02286c5a5b12e17766b7410bd)) -* **nodemon:** disable native fetch in forked node processes ([d946271](https://github.com/Financial-Times/dotcom-tool-kit/commit/d946271d80662812f017a6b2d897535dee9d2ddc)) -* specify Node 18 support in all packages' engines fields ([3b55c79](https://github.com/Financial-Times/dotcom-tool-kit/commit/3b55c79f3f55b448f1a92fcf842dab6a8906ea70)) - - -### Bug Fixes - -* **types:** make regex for determining release tags stricter ([3c85027](https://github.com/Financial-Times/dotcom-tool-kit/commit/3c8502786cfcac837ba3b301ee90a348753c5b41)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/error bumped from ^3.0.0 to ^3.1.0 - * @dotcom-tool-kit/logger bumped from ^3.0.0 to ^3.1.0 - -## [3.0.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.10.0...types-v3.0.0) (2023-04-18) - - -### ⚠ BREAKING CHANGES - -* drop support for Node 14 across all packages - -### Miscellaneous Chores - -* drop support for Node 14 across all packages ([aaee178](https://github.com/Financial-Times/dotcom-tool-kit/commit/aaee178b535a51f9c75a882d78ffd8e8aa3eac60)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/error bumped from ^2.0.0 to ^3.0.0 - * @dotcom-tool-kit/logger bumped from ^2.2.1 to ^3.0.0 - -## [2.10.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.9.2...types-v2.10.0) (2023-04-05) - - -### Features - -* **serverless:** add ServerlessDeploy task ([cd23f88](https://github.com/Financial-Times/dotcom-tool-kit/commit/cd23f88ce453a48dec393dc2645c7a22948e3944)) - -## [2.9.2](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.9.1...types-v2.9.2) (2023-03-22) - - -### Bug Fixes - -* **types:** make vault options optional ([f2e9cc0](https://github.com/Financial-Times/dotcom-tool-kit/commit/f2e9cc0b45d79c26c5ee5ec248fe44f073900835)) - -## [2.9.1](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.9.0...types-v2.9.1) (2023-03-15) - - -### Bug Fixes - -* **types:** allow files options to be simple strings as well as arrays ([3dc32c0](https://github.com/Financial-Times/dotcom-tool-kit/commit/3dc32c041849d8718861fbc0e0d3b72c026804c8)) - -## [2.9.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.8.0...types-v2.9.0) (2023-03-07) - - -### Features - -* add serverless plugin ([2041b7d](https://github.com/Financial-Times/dotcom-tool-kit/commit/2041b7d65c941823f59cbba61b11d32fe67ed906)) -* handle default option values with zod ([7c03517](https://github.com/Financial-Times/dotcom-tool-kit/commit/7c0351771cf1a3d795803295a41dfea755176b19)) -* **serverless:** define ServerlessProvision task ([6f49aaa](https://github.com/Financial-Times/dotcom-tool-kit/commit/6f49aaa80bb315e5dfd11068a21cb1d3e52ef36a)) -* **types:** use Zod for option schemas ([adc1643](https://github.com/Financial-Times/dotcom-tool-kit/commit/adc16437cf0977595b0d0c8b02337b78ee02b2b2)) -* validate plugin options with zod ([5164050](https://github.com/Financial-Times/dotcom-tool-kit/commit/5164050869958284611e0fa489551521201e6ac4)) - - -### Bug Fixes - -* **types:** allow arbitrary parameters to be passed to CircleCI jobs ([85cc8eb](https://github.com/Financial-Times/dotcom-tool-kit/commit/85cc8ebd9eafbe2de848dfe1c09bb320866910fb)) -* **types:** export jest and pa11y schemas that were previously missing ([11e7a1f](https://github.com/Financial-Times/dotcom-tool-kit/commit/11e7a1f30fccf7fc31c71c9867cab4f1754db34f)) -* **types:** make sure to export serverless schema type ([69584aa](https://github.com/Financial-Times/dotcom-tool-kit/commit/69584aa4f6f17172bda9714d0155a2517cba4121)) -* **types:** use more precise CircleCI configuration interface types ([2e4bf10](https://github.com/Financial-Times/dotcom-tool-kit/commit/2e4bf10157c3c321efd63b14aa5ebb1d38da9550)) -* **upload-assets-to-s3:** allow setting region for uploads ([89a984d](https://github.com/Financial-Times/dotcom-tool-kit/commit/89a984db001d6388eada79934d16bb9ad75c98e9)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/logger bumped from ^2.2.0 to ^2.2.1 - -## [2.8.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.7.1...types-v2.8.0) (2023-01-04) - - -### Features - -* **typescript:** add typescript plugin ([0421bdb](https://github.com/Financial-Times/dotcom-tool-kit/commit/0421bdba1f3a56fc8306b8c487433e54b740905c)) - -## [2.7.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.6.2...types-v2.7.0) (2022-12-08) - - -### Features - -* **circleci-heroku:** add support for using a Cypress docker image ([59f914a](https://github.com/Financial-Times/dotcom-tool-kit/commit/59f914aefdb7beae5e8ea0fac314efbc7194d802)) -* **circleci:** only print jobs that are missing in error ([c75c3ad](https://github.com/Financial-Times/dotcom-tool-kit/commit/c75c3ad6d91fbc5779d2a3fbed853f474babfad0)) -* **cli:** allow state to be shared between install hooks ([aaa5331](https://github.com/Financial-Times/dotcom-tool-kit/commit/aaa533123a48fe9168ec666edeabdd7a8c7428a6)) -* **cypress:** add plugin for running cypress locally and in the CI ([870a50b](https://github.com/Financial-Times/dotcom-tool-kit/commit/870a50b107bfa1f1846d35ba074fd3088cc63563)) - - -### Bug Fixes - -* **upload-assets-to-s3:** handle AWS keys correctly ([a52db39](https://github.com/Financial-Times/dotcom-tool-kit/commit/a52db39253108cd53494a3cffea043e8e89bdbf7)) - - -### Performance Improvements - -* improve lodash tree shaking ([454f9cd](https://github.com/Financial-Times/dotcom-tool-kit/commit/454f9cd9984162141c7318165d723593295db678)) - -### [2.6.2](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.6.1...types-v2.6.2) (2022-11-09) - - -### Bug Fixes - -* add tslib to individual plugins ([142363e](https://github.com/Financial-Times/dotcom-tool-kit/commit/142363edb2a82ebf4dc3c8e1b392888ebfd7dc89)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/error bumped from ^2.0.0 to ^2.0.1 - * @dotcom-tool-kit/logger bumped from ^2.1.1 to ^2.1.2 - -### [2.6.1](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.6.0...types-v2.6.1) (2022-09-21) - - -### Bug Fixes - -* prettier plugin respects .prettierignore ([2a15eab](https://github.com/Financial-Times/dotcom-tool-kit/commit/2a15eab2432cf9b0464bc3c4023f59f136350059)) - -## [2.6.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.5.1...types-v2.6.0) (2022-09-14) - - -### Features - -* deprecate config in favour for options in eslint ([831324a](https://github.com/Financial-Times/dotcom-tool-kit/commit/831324a40df17ca947fc000f51e011a2e79a4f91)) -* **node:** allow specifying ports ([20f797a](https://github.com/Financial-Times/dotcom-tool-kit/commit/20f797a9d547863c2e5fd3a40948ec62e575cbf8)) -* **node:** make vault optional ([cd12619](https://github.com/Financial-Times/dotcom-tool-kit/commit/cd12619346cfc92d67325c7ec4065a228e414f8c)) -* **nodemon:** allow specifying preferred ports ([0aab812](https://github.com/Financial-Times/dotcom-tool-kit/commit/0aab812dfab4eb778c5007eb6ddb2db99a9cc3b2)) -* **nodemon:** make vault optional ([9d28d95](https://github.com/Financial-Times/dotcom-tool-kit/commit/9d28d95b7b76fea14741f484d08abc19dc522911)) - -## [2.5.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.4.0...types-v2.5.0) (2022-07-27) - - -### Features - -* **types:** update Pa11y schema type ([8feb5fb](https://github.com/Financial-Times/dotcom-tool-kit/commit/8feb5fb685536805ae188e44c8905c5fe498ba4c)) - -## [2.4.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.3.0...types-v2.4.0) (2022-07-21) - - -### Features - -* add validation errors to compatibility test in types package ([ac118b3](https://github.com/Financial-Times/dotcom-tool-kit/commit/ac118b37bdbb30e062a2d559cc37dc36af4bcb73)) -* add validation errors to compatibility test in types package ([5228fab](https://github.com/Financial-Times/dotcom-tool-kit/commit/5228fab26ccee26bd786480bf280f6b91965679f)) -* loosen task and hook instance checks ([ac118b3](https://github.com/Financial-Times/dotcom-tool-kit/commit/ac118b37bdbb30e062a2d559cc37dc36af4bcb73)) -* loosen task and hook instance checks ([d79b7cf](https://github.com/Financial-Times/dotcom-tool-kit/commit/d79b7cfb5aed68be8b451dd2961f1abe3624c7b9)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/logger bumped from ^2.0.0 to ^2.1.0 - -## [2.3.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.2.0...types-v2.3.0) (2022-06-20) - - -### Features - -* **mocha:** allow plugin to pick up .mocharc files ([1ef9fd5](https://github.com/Financial-Times/dotcom-tool-kit/commit/1ef9fd51a50c4a7b53a9655befcb5943838bae97)) -* **node:** allow arbitrary arguments to be set for node process ([32cfe94](https://github.com/Financial-Times/dotcom-tool-kit/commit/32cfe946c49236e2170b625f49152f8f30ab1a15)) - -## [2.2.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.1.0...types-v2.2.0) (2022-06-07) - - -### Features - -* add options to pa11y plugin ([7670db7](https://github.com/Financial-Times/dotcom-tool-kit/commit/7670db7f59e9a798b5fc256182534c5c696f700a)) - -## [2.1.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v2.0.0...types-v2.1.0) (2022-05-06) - - -### Features - -* **webpack:** add watch mode command for run:local by default ([001a881](https://github.com/Financial-Times/dotcom-tool-kit/commit/001a881c85e5e123cc43075e367c3825c0538d4f)) - -## [2.0.0](https://github.com/Financial-Times/dotcom-tool-kit/compare/types-v1.9.0...types-v2.0.0) (2022-04-19) - - -### Miscellaneous Chores - -* release 2.0 version for all packages ([42dc5d3](https://github.com/Financial-Times/dotcom-tool-kit/commit/42dc5d39bf330b9bca4121d062470904f9c6918d)) - - -### Dependencies - -* The following workspace dependencies were updated - * dependencies - * @dotcom-tool-kit/error bumped from ^1.9.0 to ^2.0.0 diff --git a/lib/types/src/circleci.ts b/lib/types/src/circleci.ts deleted file mode 100644 index e02526d8d..000000000 --- a/lib/types/src/circleci.ts +++ /dev/null @@ -1,65 +0,0 @@ -export const automatedComment = '# CONFIG GENERATED BY DOTCOM-TOOL-KIT, DO NOT EDIT BY HAND\n' - -export type JobConfig = { - type?: string - docker?: { image: string; environment?: Record }[] - context?: string | string[] - requires?: string[] - filters?: { branches?: { only?: string; ignore?: string }; tags?: { only?: string } } - executor?: string - [parameter: string]: unknown -} - -type TriggerConfig = { - schedule?: { cron: string; filters?: { branches: { only?: string; ignore?: string } } } -} - -export type Job = string | { [job: string]: JobConfig } - -export type Trigger = string | { [trigger: string]: TriggerConfig } - -export type Workflow = { - jobs?: Job[] - triggers?: Trigger[] -} - -export interface CircleConfig { - version: 2.1 - orbs: { - [orb: string]: string - } - executors: { - [executor: string]: { - docker: { image: string }[] - } - } - jobs: { - [job: string]: { - docker: { image: string }[] - steps: (string | { [command: string]: { path?: string } })[] - } - } - workflows: { - 'tool-kit': { - when: { - not: { - equal: ['scheduled_pipeline', '<< pipeline.trigger_source >>'] - } - } - jobs: Job[] - } - nightly: { - when: { - and: [ - { - equal: ['scheduled_pipeline', '<< pipeline.trigger_source >>'] - }, - { - equal: ['nightly', '<< pipeline.schedule.name >>'] - } - ] - } - jobs: Job[] - } - } -} diff --git a/lib/types/src/index.ts b/lib/types/src/index.ts deleted file mode 100644 index 00f09908d..000000000 --- a/lib/types/src/index.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { styles as s } from '@dotcom-tool-kit/logger' -import fs from 'fs' -import path from 'path' -import semver from 'semver' -import type { Logger } from 'winston' -import { z } from 'zod' - -const packageJsonPath = path.resolve(__dirname, '../package.json') -const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) -const version: string = packageJson.version - -// uses Symbol.for, not Symbol, so that they're compatible across different -// @dotcom-tool-kit/types instances - -// used as the name for the property we use to identify classes -const typeSymbol = Symbol.for('@dotcom-tool-kit/types') - -// used to identify the Base, Task and Hook classes -const baseSymbol = Symbol.for('@dotcom-tool-kit/types/base') -const taskSymbol = Symbol.for('@dotcom-tool-kit/types/task') -const hookSymbol = Symbol.for('@dotcom-tool-kit/types/hook') - -export interface Invalid { - valid: false - reasons: string[] -} -export interface Valid { - valid: true - value: T -} -export type Validated = Invalid | Valid - -export function mapValidated(validated: Validated, f: (val: T) => U): Validated { - if (validated.valid) { - return { valid: true, value: f(validated.value) } - } else { - return validated - } -} - -export function mapValidationError( - validated: Validated, - f: (reasons: string[]) => string[] -): Validated { - if (validated.valid) { - return validated - } else { - return { valid: false, reasons: f(validated.reasons) } - } -} - -export function joinValidated(first: Validated, second: Validated): Validated<[T, U]> { - if (first.valid) { - if (second.valid) { - return { valid: true, value: [first.value, second.value] } - } else { - return second - } - } else { - if (second.valid) { - return first - } else { - return { valid: false, reasons: [...first.reasons, ...second.reasons] } - } - } -} - -export function reduceValidated(validated: Validated[]): Validated { - let sequenced: Validated = { valid: true, value: [] } - for (const val of validated) { - if (sequenced.valid) { - if (val.valid) { - sequenced.value.push(val.value) - } else { - sequenced = { valid: false, reasons: val.reasons } - } - } else if (!val.valid) { - sequenced.reasons.push(...val.reasons) - } - } - return sequenced -} - -abstract class Base { - static version = version - version = version - - static get [typeSymbol](): symbol { - return baseSymbol - } - - get [typeSymbol](): symbol { - return baseSymbol - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - static is(objectToCheck: any): objectToCheck is T { - return objectToCheck[typeSymbol] === this[typeSymbol] - } - - static isCompatible(objectToCheck: unknown): Validated { - if (!this.is(objectToCheck)) { - return { - valid: false, - reasons: [ - `${s.plugin( - '@dotcom-tool-kit/types' - )} type symbol is missing, make sure that this object derives from the ${s.code( - 'Task' - )} or ${s.code('Hook')} class defined by the plugin` - ] - } - } - - // an 'objectToCheck' from a plugin is compatible with this CLI if its - // version is semver-compatible with the @dotcom-tool-kit/types included by - // the CLI (which is what's calling this). so, prepend ^ to our version, - // and check our version satisfies that. - - // this lets e.g. a CLI that includes types@2.2.0 load any plugin - // that depends on any higher minor version of types. - const range = `^${this.version}` - if (semver.satisfies(objectToCheck.version, range)) { - return { valid: true, value: objectToCheck as T } - } else { - return { - valid: false, - reasons: [ - `object is from an outdated version of ${s.plugin( - '@dotcom-tool-kit/types' - )}, make sure you're using at least version ${s.heading(this.version)} of the plugin` - ] - } - } - } -} - -export abstract class Task extends Base { - static description: string - static plugin?: Plugin - static id?: string - - static get [typeSymbol](): symbol { - return taskSymbol - } - - get [typeSymbol](): symbol { - return taskSymbol - } - - options: z.output - logger: Logger - - constructor(logger: Logger, options: z.output) { - super() - - const staticThis = this.constructor as typeof Task - this.options = options - this.logger = logger.child({ task: staticThis.id }) - } - - abstract run(files?: string[]): Promise -} - -export type TaskClass = { - new (logger: Logger, options: Partial>): Task -} & typeof Task - -export abstract class Hook extends Base { - id?: string - plugin?: Plugin - logger: Logger - static description?: string - // This field is used to collect hooks that share state when running their - // install methods. All hooks in the same group will run their install method - // one after the other, and then their commitInstall method will be run with - // the collected state. - installGroup?: string - - static get [typeSymbol](): symbol { - return hookSymbol - } - - get [typeSymbol](): symbol { - return hookSymbol - } - - constructor(logger: Logger) { - super() - - this.logger = logger.child({ hook: this.constructor.name }) - } - - abstract check(): Promise - abstract install(state?: State): Promise - // Intentional unused parameter as pre-fixed with an underscore - // eslint-disable-next-line no-unused-vars - async commitInstall(_state: State): Promise { - return - } -} - -export type HookConstructor = { new (logger: Logger): Hook } - -export type RCFile = { - plugins: string[] - hooks: { [id: string]: string | string[] } - options: { [id: string]: Record } -} - -export interface Plugin { - id: string - root: string - rcFile?: RCFile - module?: PluginModule - parent?: Plugin - children?: Plugin[] -} - -export interface PluginModule { - tasks: TaskClass[] - hooks: { - [id: string]: HookConstructor - } -} diff --git a/lib/types/src/npm.ts b/lib/types/src/npm.ts deleted file mode 100644 index 20943f0c0..000000000 --- a/lib/types/src/npm.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const semVerRegex = /^v\d+\.\d+\.\d+(-.+)?/ -export const prereleaseRegex = /^v\d+\.\d+\.\d+(?:-\w+\.\d+)$/ -export const releaseRegex = /^v\d+\.\d+\.\d+$/ diff --git a/lib/types/src/schema.ts b/lib/types/src/schema.ts deleted file mode 100644 index 75dbba574..000000000 --- a/lib/types/src/schema.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type prompts from 'prompts' -import type { Logger } from 'winston' -import { z } from 'zod' - -import { BizOpsSystem } from './bizOps' - -/** - * A function that should use the `prompt` parameter passed to build a more - * complex option structure, like a nested object, from user input. Returning - * an undefined value will cause the program to fall back to the default prompt - * interface. - * @param onCancel - pass this to `prompt`'s options so that a user - * interrupting the prompt can be handled properly - */ -export type SchemaPromptGenerator = ( - logger: Logger, - prompt: typeof prompts, - onCancel: () => void, - // HACK:20231209:IM add bizOpsSystem as optional parameter to maintain - // backwards compatibility - bizOpsSystem?: BizOpsSystem -) => Promise -// This type defines an interface you can use to export prompt generators. The -// `T` type parameter should be the type of your `Schema` object, and it will -// be mapped into a partial object of `SchemaPromptGenerator` functions with -// all their return types set to the output type of each option schema. -export type PromptGenerators = T extends z.ZodObject - ? { - [option in keyof Shape as Shape[option] extends z.ZodType - ? option - : never]?: Shape[option] extends z.ZodType ? SchemaPromptGenerator> : never - } - : never - -import { BabelSchema } from './schema/babel' -import { CircleCISchema } from './schema/circleci' -import { CypressSchema } from './schema/cypress' -import { DopplerSchema } from './schema/doppler' -import { RootSchema } from './schema/dotcom-tool-kit' -import { ESLintSchema } from './schema/eslint' -import { HerokuSchema } from './schema/heroku' -import { LintStagedNpmSchema } from './schema/lint-staged-npm' -import { JestSchema } from './schema/jest' -import { MochaSchema } from './schema/mocha' -import { SmokeTestSchema } from './schema/n-test' -import { NextRouterSchema } from './schema/next-router' -import { NodeSchema } from './schema/node' -import { NodemonSchema } from './schema/nodemon' -import { Pa11ySchema } from './schema/pa11y' -import { PrettierSchema } from './schema/prettier' -import { ServerlessSchema } from './schema/serverless' -import { TypeScriptSchema } from './schema/typescript' -import { UploadAssetsToS3Schema } from './schema/upload-assets-to-s3' -import { VaultSchema } from './schema/vault' -import { WebpackSchema } from './schema/webpack' - -export const Schemas = { - 'app root': RootSchema, - '@dotcom-tool-kit/babel': BabelSchema, - '@dotcom-tool-kit/circleci': CircleCISchema, - '@dotcom-tool-kit/cypress': CypressSchema, - '@dotcom-tool-kit/doppler': DopplerSchema, - '@dotcom-tool-kit/eslint': ESLintSchema, - '@dotcom-tool-kit/heroku': HerokuSchema, - '@dotcom-tool-kit/lint-staged-npm': LintStagedNpmSchema, - '@dotcom-tool-kit/jest': JestSchema, - '@dotcom-tool-kit/mocha': MochaSchema, - '@dotcom-tool-kit/n-test': SmokeTestSchema, - '@dotcom-tool-kit/next-router': NextRouterSchema, - '@dotcom-tool-kit/node': NodeSchema, - '@dotcom-tool-kit/nodemon': NodemonSchema, - '@dotcom-tool-kit/pa11y': Pa11ySchema, - '@dotcom-tool-kit/prettier': PrettierSchema, - '@dotcom-tool-kit/serverless': ServerlessSchema, - '@dotcom-tool-kit/typescript': TypeScriptSchema, - '@dotcom-tool-kit/upload-assets-to-s3': UploadAssetsToS3Schema, - '@dotcom-tool-kit/vault': VaultSchema, - '@dotcom-tool-kit/webpack': WebpackSchema -} - -// Gives the TypeScript type represented by each Schema -export type Options = { - [plugin in keyof typeof Schemas]: typeof Schemas[plugin] extends z.ZodTypeAny - ? z.infer - : never -} diff --git a/lib/types/src/schema/babel.ts b/lib/types/src/schema/babel.ts deleted file mode 100644 index 98d52730f..000000000 --- a/lib/types/src/schema/babel.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from 'zod' - -export const BabelSchema = z.object({ - files: z.string().optional(), - outputPath: z.string().optional(), - configFile: z.string().optional() -}) -export type BabelOptions = z.infer - -export const Schema = BabelSchema diff --git a/lib/types/src/schema/circleci.ts b/lib/types/src/schema/circleci.ts deleted file mode 100644 index 55fc414d5..000000000 --- a/lib/types/src/schema/circleci.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { z } from 'zod' - -export const CircleCISchema = z.object({ - nodeVersion: z.string().or(z.string().array()).default('16.14-browsers'), - cypressImage: z.string().optional() -}) -export type CircleCIOptions = z.infer - -export const Schema = CircleCISchema diff --git a/lib/types/src/schema/cypress.ts b/lib/types/src/schema/cypress.ts deleted file mode 100644 index d8788ace4..000000000 --- a/lib/types/src/schema/cypress.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { z } from 'zod' - -export const CypressSchema = z.object({ - localUrl: z.string().optional() -}) -export type CypressOptions = z.infer - -export const Schema = CypressSchema diff --git a/lib/types/src/schema/eslint.ts b/lib/types/src/schema/eslint.ts deleted file mode 100644 index 4e11c42e6..000000000 --- a/lib/types/src/schema/eslint.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { z } from 'zod' - -export const ESLintSchema = z.object({ - files: z.string().array().or(z.string()).default(['**/*.js']), - config: z.record(z.unknown()).optional(), // @deprecated: use options instead - options: z.record(z.unknown()).optional() -}) -export type ESLintOptions = z.infer - -export const Schema = ESLintSchema diff --git a/lib/types/src/schema/jest.ts b/lib/types/src/schema/jest.ts deleted file mode 100644 index a14ff44b0..000000000 --- a/lib/types/src/schema/jest.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { z } from 'zod' - -export const JestSchema = z.object({ - configPath: z.string().optional() -}) -export type JestMode = 'ci' | 'local' -export type JestOptions = z.infer - -export const Schema = JestSchema diff --git a/lib/types/src/schema/mocha.ts b/lib/types/src/schema/mocha.ts deleted file mode 100644 index f52438d3b..000000000 --- a/lib/types/src/schema/mocha.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { z } from 'zod' - -export const MochaSchema = z.object({ - files: z.string().default('test/**/*.js'), - configPath: z.string().optional() -}) -export type MochaOptions = z.infer - -export const Schema = MochaSchema diff --git a/lib/types/src/schema/n-test.ts b/lib/types/src/schema/n-test.ts deleted file mode 100644 index bca12edd6..000000000 --- a/lib/types/src/schema/n-test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { z } from 'zod' - -export const SmokeTestSchema = z.object({ - browsers: z.string().array().optional(), - host: z.string().optional(), - config: z.string().optional(), - interactive: z.boolean().optional(), - header: z.record(z.string()).optional() -}) -export type SmokeTestOptions = z.infer - -export const Schema = SmokeTestSchema diff --git a/lib/types/src/schema/node.ts b/lib/types/src/schema/node.ts deleted file mode 100644 index e839c6d06..000000000 --- a/lib/types/src/schema/node.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod' - -export const NodeSchema = z.object({ - entry: z.string().default('./server/app.js'), - args: z.string().array().optional(), - useVault: z.boolean().default(true), - ports: z.number().array().default([3001, 3002, 3003]) -}) -export type NodeOptions = z.infer - -export const Schema = NodeSchema diff --git a/lib/types/src/schema/nodemon.ts b/lib/types/src/schema/nodemon.ts deleted file mode 100644 index 11a12c945..000000000 --- a/lib/types/src/schema/nodemon.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { z } from 'zod' - -export const NodemonSchema = z.object({ - entry: z.string().default('./server/app.js'), - configPath: z.string().optional(), - useVault: z.boolean().default(true), - ports: z.number().array().default([3001, 3002, 3003]) -}) -export type NodemonOptions = z.infer - -export const Schema = NodemonSchema diff --git a/lib/types/src/schema/pa11y.ts b/lib/types/src/schema/pa11y.ts deleted file mode 100644 index cd0e64039..000000000 --- a/lib/types/src/schema/pa11y.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { z } from 'zod' - -export const Pa11ySchema = z.object({ - configFile: z.string().optional() -}) -export type Pa11yOptions = z.infer - -export const Schema = Pa11ySchema diff --git a/lib/types/src/schema/prettier.ts b/lib/types/src/schema/prettier.ts deleted file mode 100644 index 98dc5417e..000000000 --- a/lib/types/src/schema/prettier.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from 'zod' - -export const PrettierSchema = z.object({ - files: z.string().array().or(z.string()).default(['**/*.{js,jsx,ts,tsx}']), - configFile: z.string().optional(), - ignoreFile: z.string().default('.prettierignore'), - configOptions: z.record(z.unknown()).default({ - singleQuote: true, - useTabs: true, - bracketSpacing: true, - arrowParens: 'always', - trailingComma: 'none' - }) -}) -export type PrettierOptions = z.infer - -export const Schema = PrettierSchema diff --git a/lib/types/src/schema/typescript.ts b/lib/types/src/schema/typescript.ts deleted file mode 100644 index 5427e7005..000000000 --- a/lib/types/src/schema/typescript.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { z } from 'zod' - -export const TypeScriptSchema = z.object({ - configPath: z.string().optional(), - extraArgs: z.string().array().optional() -}) -export type TypeScriptOptions = z.infer - -export const Schema = TypeScriptSchema diff --git a/lib/types/src/schema/upload-assets-to-s3.ts b/lib/types/src/schema/upload-assets-to-s3.ts deleted file mode 100644 index 9906ccc50..000000000 --- a/lib/types/src/schema/upload-assets-to-s3.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { z } from 'zod' - -export const UploadAssetsToS3Schema = z.object({ - accessKeyIdEnvVar: z.string().optional(), - secretAccessKeyEnvVar: z.string().optional(), - accessKeyId: z.string().default('aws_access_hashed_assets'), // @deprecated: use accessKeyIdEnvVar instead - secretAccessKey: z.string().default('aws_secret_hashed_assets'), // @deprecated: use secretAccessKeyEnvVar instead - directory: z.string().default('public'), - reviewBucket: z.string().array().default(['ft-next-hashed-assets-preview']), - prodBucket: z.string().array().default(['ft-next-hashed-assets-prod']), - region: z.string().default('eu-west-1'), - destination: z.string().default('hashed-assets/page-kit'), - extensions: z.string().default('js,css,map,gz,br,png,jpg,jpeg,gif,webp,svg,ico,json'), - cacheControl: z.string().default('public, max-age=31536000, stale-while-revalidate=60, stale-if-error=3600') -}) -export type UploadAssetsToS3Options = z.infer - -export const Schema = UploadAssetsToS3Schema diff --git a/lib/types/src/schema/webpack.ts b/lib/types/src/schema/webpack.ts deleted file mode 100644 index d14c5ad13..000000000 --- a/lib/types/src/schema/webpack.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { z } from 'zod' - -export const WebpackSchema = z.object({ - configPath: z.string().optional() -}) -export type WebpackOptions = z.infer - -export const Schema = WebpackSchema diff --git a/lib/validated/README.md b/lib/validated/README.md new file mode 100644 index 000000000..576046bbc --- /dev/null +++ b/lib/validated/README.md @@ -0,0 +1,67 @@ +# `@dotcom-tool-kit/validated` + +Types and functions for an errors-as-first-class-values pattern + +## Rationale + +In Tool Kit, we handle loading many plugins with different types of entry point (for `Task`s, `Hook`s etc) in parallel. There are various points this process could go wrong: importing an entry point, checking version compatibility, and more. + +If we naïvely used exception throwing or Promise rejections, Tool Kit would stop at the first problem that happened; the user would fix that only to run into the next error, which would be frustrating. We want to get as far as we can and collect all the errors that happen to present _once_, as late as possible. + +We _could_ still use exceptions/rejections for this, along with `Promise.allSettled`. However, with that pattern it would be difficult to follow the data flow of the errors, and easy to accidentally forget to collate some errors, falling into the same issue of throwing one error at a time. + +Instead, we model these errors using an explicit return value, `Validated`. This type is a union of `Valid` which represents a correct value, and `Invalid` which represents a problem with a list of `string` reasons. By making this an explicit type, we clearly mark all the functions that can return errors we may want to collate, and make it impossible to use the values without deciding what to do with the errors. + +This pattern is modelled after Go's [error handling using multiple return values](https://go.dev/doc/tutorial/handle-errors), Rust's [`Result` type](https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html) and Haskell's [`Either a b` type](https://hackage.haskell.org/package/base-4.19.0.0/docs/Data-Either.html). + +## Usage + +### Creating a `Validated` + +`@dotcom-tool-kit/validated` exports two constructor functions, `valid` and `invalid`. Use these to create a wrapper when the status of the value is known, e.g.: + +```typescript +valid({ some: 'object' }) + +invalid([ + 'list of problems' +]) +``` + +### Using `Validated` values + +`Validated` objects have methods that are similar to the Array or Promise methods, allowing you to do things with the values or error reasons without having to inspect the types: + +```typescript +validated.map(value => { + /* do something with value */ + // `return` something else. replaces the value with this return value if valid, does nothing if invalid +}) + +validated.mapError(reasons => { + /* do something with reasons */ + // `return` something else. replaces the reasons with this return value if invalid, does nothing if valid +}) +``` + +### Extracting values from a `Validated` + +`Validated` has an `.unwrap` method that allows you to extract its value if valid, or throws an error with its reasons if invalid. This should be used as late as possible, when you can't get any further without the value, and need to stop if it's invalid. + +```typescript +validated.unwrap('something was invalid!') +``` + +### Grouping an array of `Validated` + +Similar to `Promise.all`, if you have multiple `Validated`s and you need to group their values and reasons (i.e. you have `Array>` and you want `Validated>`), we have `reduceValidated`: + +```typescript +reduceValidated([ + validated1, + validated2, + validated3 +]) +``` + +This is used in Tool Kit when e.g. we load multiple plugins in parallel. diff --git a/lib/types/jest.config.js b/lib/validated/jest.config.js similarity index 80% rename from lib/types/jest.config.js rename to lib/validated/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/lib/types/jest.config.js +++ b/lib/validated/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/lib/validated/package.json b/lib/validated/package.json new file mode 100644 index 000000000..4b0e33118 --- /dev/null +++ b/lib/validated/package.json @@ -0,0 +1,14 @@ +{ + "name": "@dotcom-tool-kit/validated", + "version": "2.0.0-beta.0", + "description": "", + "main": "lib", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@dotcom-tool-kit/error": "4.0.0-beta.0" + } +} diff --git a/lib/validated/src/index.ts b/lib/validated/src/index.ts new file mode 100644 index 000000000..fc48c251a --- /dev/null +++ b/lib/validated/src/index.ts @@ -0,0 +1,86 @@ +import { ToolKitError } from '@dotcom-tool-kit/error' + +interface Mixin { + map(f: (val: T) => U): Validated + mapError(f: (reasons: string[]) => string[]): Validated + flatMap(f: (val: T) => Validated): Validated + awaitValue(): Promise>> + unwrap(message?: string): T +} + +export type Invalid = { + valid: false + reasons: string[] +} + +export type Valid = { + valid: true + value: T +} + +export type Validated = (Invalid | Valid) & Mixin + +export const invalid = (reasons: string[]) => mixin({ valid: false, reasons }) +export const valid = (value: T) => mixin({ valid: true, value }) + +const mixin = (validated: Invalid | Valid): Validated => ({ + ...validated, + + map(f) { + if (validated.valid) { + return valid(f(validated.value)) + } else { + return invalid(validated.reasons) + } + }, + + mapError(f) { + if (validated.valid) { + return mixin(validated) + } else { + return invalid(f(validated.reasons)) + } + }, + + flatMap(f) { + if (validated.valid) { + return f(validated.value) + } else { + return mixin(validated) + } + }, + + unwrap(message = '') { + if (validated.valid) { + return validated.value + } else { + const error = new ToolKitError(message) + error.details = validated.reasons.join('\n\n') + throw error + } + }, + + async awaitValue() { + if (validated.valid) { + return valid(await validated.value) + } else { + return invalid(validated.reasons) + } + } +}) + +export function reduceValidated(validated: Validated[]): Validated { + let sequenced: Validated = valid([]) + for (const val of validated) { + if (sequenced.valid) { + if (val.valid) { + sequenced.value.push(val.value) + } else { + sequenced = invalid(val.reasons) + } + } else if (!val.valid) { + sequenced.reasons.push(...val.reasons) + } + } + return sequenced +} diff --git a/lib/validated/test/index.test.ts b/lib/validated/test/index.test.ts new file mode 100644 index 000000000..0c8624cb9 --- /dev/null +++ b/lib/validated/test/index.test.ts @@ -0,0 +1,156 @@ +import { describe, expect, it, test } from '@jest/globals' +import { invalid, reduceValidated, valid } from '../src' + +describe('Validated', () => { + describe('constructor functions', () => { + test('valid', () => { + const validated = valid({ some: 'object ' }) + expect(validated).toHaveProperty('valid', true) + expect(validated).toHaveProperty('value', { some: 'object ' }) + }) + + test('invalid', () => { + const validated = invalid(['reasons']) + expect(validated).toHaveProperty('valid', false) + expect(validated).toHaveProperty('reasons', ['reasons']) + }) + }) + + describe('map', () => { + it('should map value of valid', () => { + expect(valid(5).map((value) => value * 2)).toEqual( + expect.objectContaining({ + valid: true, + value: 10 + }) + ) + }) + + it('should do nothing for invalid', () => { + expect( + invalid(['hello']).map((value) => value * 2) + ).toEqual( + expect.objectContaining({ + valid: false, + reasons: ['hello'] + }) + ) + }) + }) + + describe('mapError', () => { + it('should do nothing for valid', () => { + expect(valid(5).mapError((reasons) => reasons.concat('another reason'))).toEqual( + expect.objectContaining({ + valid: true, + value: 5 + }) + ) + }) + + it('should map reasons of invalid', () => { + expect( + invalid(['hello']).mapError((reasons) => reasons.concat('another reason')) + ).toEqual( + expect.objectContaining({ + valid: false, + reasons: ['hello', 'another reason'] + }) + ) + }) + }) + + describe('flatMap', () => { + it('should map value of valid when returning valid', () => { + expect(valid(5).flatMap((value) => valid(value * 2))).toEqual( + expect.objectContaining({ + valid: true, + value: 10 + }) + ) + }) + + it('should map value of valid when returning invalid', () => { + expect(valid(5).flatMap((value) => invalid([`no ${value}`]))).toEqual( + expect.objectContaining({ + valid: false, + reasons: ['no 5'] + }) + ) + }) + + it('should do nothing for invalid', () => { + expect( + invalid(['hello']).flatMap((value) => valid(value * 2)) + ).toEqual( + expect.objectContaining({ + valid: false, + reasons: ['hello'] + }) + ) + }) + }) + + describe('awaitValue', () => { + it('should await the value of a valid', async () => { + expect(await valid(Promise.resolve(5)).awaitValue()).toEqual( + expect.objectContaining({ + valid: true, + value: 5 + }) + ) + }) + + it('should do nothing for invalid', async () => { + expect(await invalid(['hello']).awaitValue()).toEqual( + expect.objectContaining({ + valid: false, + reasons: ['hello'] + }) + ) + }) + }) + + describe('unwrap', () => { + it('should return the value of a valid', () => { + expect(valid(5).unwrap()).toBe(5) + }) + + it('should throw message and reasons for invalid', () => { + expect(() => invalid(['hello', 'there']).unwrap('invalid!')).toThrowError( + expect.objectContaining({ + message: 'invalid!', + details: 'hello\n\nthere' + }) + ) + }) + }) + + describe('reduceValidated', () => { + it('should return valid with array if all are valid', () => { + expect(reduceValidated([valid(1), valid(2), valid(3)])).toEqual( + expect.objectContaining({ + valid: true, + value: [1, 2, 3] + }) + ) + }) + + it('should return invalid concatenating reasons if any are invalid', () => { + expect( + reduceValidated([ + valid(1), + invalid(['hello', 'there']), + valid(2), + invalid(['general', 'kenobi']), + valid(3) + ]) + ).toEqual( + expect.objectContaining({ + valid: false, + reasons: ['hello', 'there', 'general', 'kenobi'] + }) + ) + }) + }) +}) diff --git a/lib/types/tsconfig.json b/lib/validated/tsconfig.json similarity index 77% rename from lib/types/tsconfig.json rename to lib/validated/tsconfig.json index 917152a25..7399e5867 100644 --- a/lib/types/tsconfig.json +++ b/lib/validated/tsconfig.json @@ -1,15 +1,13 @@ { "extends": "../../tsconfig.settings.json", + "include": ["src/**/*"], + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, "references": [ { "path": "../error" - }, - { - "path": "../logger" } - ], - "compilerOptions": { - "outDir": "lib", - "rootDir": "src" - } + ] } diff --git a/lib/vault/.toolkitrc.yml b/lib/vault/.toolkitrc.yml index e69de29bb..d48cfb24d 100644 --- a/lib/vault/.toolkitrc.yml +++ b/lib/vault/.toolkitrc.yml @@ -0,0 +1,2 @@ + +version: 2 diff --git a/lib/vault/jest.config.js b/lib/vault/jest.config.js index 8ae711e3d..91d4858ef 100644 --- a/lib/vault/jest.config.js +++ b/lib/vault/jest.config.js @@ -1,7 +1,6 @@ const base = require('../../jest.config.base') module.exports = { - ...base, - collectCoverage: true, + ...base.config, moduleDirectories: ['node_modules'] } diff --git a/lib/vault/package.json b/lib/vault/package.json index 5f30a722a..6d0c8db5e 100644 --- a/lib/vault/package.json +++ b/lib/vault/package.json @@ -1,15 +1,14 @@ { "name": "@dotcom-tool-kit/vault", - "version": "3.2.0", + "version": "4.0.0-beta.0", "description": "", "main": "lib", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/options": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/options": "4.0.0-beta.0", "@financial-times/n-fetch": "^1.0.0", "fs": "0.0.1-security", "os": "^0.1.2", @@ -35,11 +34,12 @@ "extends": "../../package.json" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@types/jest": "^27.0.2", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/lib/vault/src/index.ts b/lib/vault/src/index.ts index bd4a8ef96..25d425fb3 100644 --- a/lib/vault/src/index.ts +++ b/lib/vault/src/index.ts @@ -5,7 +5,7 @@ import os from 'os' import type { Logger } from 'winston' import { ToolKitError } from '@dotcom-tool-kit/error' import { getOptions } from '@dotcom-tool-kit/options' -import { VaultOptions } from '@dotcom-tool-kit/types/lib/schema/vault' +import { VaultOptions } from '@dotcom-tool-kit/schemas/lib/plugins/vault' const VAULT_ROLE_ID = process.env.VAULT_ROLE_ID const VAULT_SECRET_ID = process.env.VAULT_SECRET_ID diff --git a/lib/vault/test/index.test.ts b/lib/vault/test/index.test.ts index a98b39e10..b7395d338 100644 --- a/lib/vault/test/index.test.ts +++ b/lib/vault/test/index.test.ts @@ -1,11 +1,10 @@ import { describe, it, beforeAll, beforeEach, afterAll, jest, expect } from '@jest/globals' import { VaultEnvVars } from '../src/index' import fetch from '@financial-times/n-fetch' -import { mocked } from 'ts-jest/utils' import fs from 'fs' import winston, { Logger } from 'winston' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger let CIRCLECI: string if (process.env.CIRCLECI) { @@ -15,7 +14,7 @@ const VAULT_AUTH_GITHUB_TOKEN = process.env.VAULT_AUTH_GITHUB_TOKEN || undefined jest.mock('@financial-times/n-fetch') -const mockedFetch = mocked(fetch, true) +const mockedFetch = jest.mocked(fetch, true) jest.mock('path', () => { return { diff --git a/lib/vault/tsconfig.json b/lib/vault/tsconfig.json index a8ef7bf3b..e2e650b6b 100644 --- a/lib/vault/tsconfig.json +++ b/lib/vault/tsconfig.json @@ -6,6 +6,9 @@ }, { "path": "../options" + }, + { + "path": "../schemas" } ], "compilerOptions": { diff --git a/lib/wait-for-ok/package.json b/lib/wait-for-ok/package.json index 91d06075c..427af3c5d 100644 --- a/lib/wait-for-ok/package.json +++ b/lib/wait-for-ok/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/wait-for-ok", - "version": "3.2.0", + "version": "4.0.0-beta.0", "description": "", "main": "lib", "scripts": { @@ -33,7 +33,7 @@ "extends": "../../package.json" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/package-lock.json b/package-lock.json index 3e6478616..ae668d2e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,65 +29,61 @@ "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", "check-engines": "^1.5.0", + "endent": "^2.1.0", "eslint": "^8.37.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.5", "husky": "^4.3.8", - "jest": "^27.4.7", + "jest": "^29.7.0", "lint-staged": "^10.5.4", - "prettier": "2.2.1", + "prettier": "^2.8.8", "release-please": "^15.0.0", - "ts-jest": "^27.1.3", - "typescript": "~4.9.5" + "ts-jest": "^29.1.2", + "typescript": "~5.4.5", + "winston": "^3.13.0", + "zod2md": "^0.1.2" }, "engines": { - "node": "16.x || 18.x || 20.x", - "npm": "7.x || 8.x || 9.x || 10.x" + "node": "18.x || 20.x", + "npm": "8.x || 9.x || 10.x" } }, "core/cli": { "name": "dotcom-tool-kit", - "version": "3.4.0", - "license": "MIT", - "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/options": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", - "@dotcom-tool-kit/wait-for-ok": "^3.2.0", - "cosmiconfig": "^7.0.0", + "version": "4.0.0-beta.5", + "license": "MIT", + "dependencies": { + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/config": "2.0.0-beta.0", + "@dotcom-tool-kit/conflict": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", + "@dotcom-tool-kit/validated": "2.0.0-beta.0", + "@dotcom-tool-kit/wait-for-ok": "4.0.0-beta.0", + "endent": "^2.1.0", "lodash": "^4.17.21", "minimist": "^1.2.5", - "resolve-from": "^5.0.0", "tslib": "^2.3.1", - "yaml": "^1.10.2", - "zod-validation-error": "^0.3.0" + "yaml": "^2.4.1", + "zod-validation-error": "^2.1.0" }, "bin": { "dotcom-tool-kit": "bin/run" }, "devDependencies": { - "@dotcom-tool-kit/babel": "^3.2.0", - "@dotcom-tool-kit/backend-heroku-app": "^3.1.0", - "@dotcom-tool-kit/circleci": "^5.4.0", - "@dotcom-tool-kit/circleci-deploy": "^3.3.0", - "@dotcom-tool-kit/eslint": "^3.2.0", - "@dotcom-tool-kit/frontend-app": "^3.2.0", - "@dotcom-tool-kit/heroku": "^3.4.0", - "@dotcom-tool-kit/mocha": "^3.2.0", - "@dotcom-tool-kit/n-test": "^3.3.0", - "@dotcom-tool-kit/npm": "^3.3.0", - "@dotcom-tool-kit/webpack": "^3.2.0", "@jest/globals": "^27.4.6", "@types/lodash": "^4.14.185", "@types/node": "^16.18.23", "chai": "^4.3.4", "globby": "^10.0.2", "ts-node": "^8.10.2", - "winston": "^3.5.1" + "winston": "^3.5.1", + "zod": "^3.22.4" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } }, @@ -120,17 +116,40 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, + "core/cli/node_modules/yaml": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz", + "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "core/cli/node_modules/zod-validation-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-2.1.0.tgz", + "integrity": "sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.18.0" + } + }, "core/create": { "name": "@dotcom-tool-kit/create", - "version": "3.5.2", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { "@aws-sdk/client-iam": "^3.282.0", "@aws-sdk/client-sts": "^3.282.0", - "@dotcom-tool-kit/doppler": "^1.1.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@octokit/rest": "^19.0.5", "@quarterto/parse-makefile-rules": "^1.1.0", "cli-highlight": "^2.1.11", @@ -144,24 +163,24 @@ "prompts": "^2.4.1", "simple-git": "^3.16.1", "tslib": "^2.3.1", - "yaml": "^2.2.1" + "yaml": "^2.2.1", + "zod": "^3.22.4" }, "bin": { "create": "bin/create-tool-kit" }, "devDependencies": { - "@types/financial-times__package-json": "^1.9.0", + "@types/financial-times__package-json": "2.0.0-beta.0", "@types/lodash": "^4.14.185", "@types/node": "^16.18.23", "@types/node-fetch": "^2.6.2", "@types/pacote": "^11.1.3", "@types/prompts": "^2.0.14", - "cosmiconfig": "^7.0.1", - "dotcom-tool-kit": "^3.4.0", + "dotcom-tool-kit": "4.0.0-beta.5", "type-fest": "^3.13.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } }, @@ -1071,52 +1090,107 @@ } }, "core/sandbox": { - "name": "@dotcom-tool-kit/sandbox", - "version": "2.0.0", - "extraneous": true, + "version": "1.0.0", + "license": "ISC", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@dotcom-tool-kit/base": "file:../../lib/base", + "@dotcom-tool-kit/node": "file:../../plugins/node", + "@dotcom-tool-kit/package-json-hook": "file:../../plugins/package-json-hook", + "@dotcom-tool-kit/parallel": "file:../../plugins/parallel", + "dotcom-tool-kit": "file:../cli" + } + }, + "lib/base": { + "name": "@dotcom-tool-kit/base", + "version": "4.0.0-beta.0", "license": "ISC", + "dependencies": { + "@dotcom-tool-kit/config": "^2.0.0-beta.0", + "@dotcom-tool-kit/conflict": "2.0.0-beta.0", + "@dotcom-tool-kit/validated": "2.0.0-beta.0", + "semver": "^7.5.4", + "winston": "^3.11.0" + }, "devDependencies": { - "@dotcom-tool-kit/circleci-heroku": "^2.0.0", - "@dotcom-tool-kit/circleci-npm": "^4.0.0", - "@dotcom-tool-kit/eslint": "^2.0.0", - "@dotcom-tool-kit/frontend-app": "^2.0.0", - "@dotcom-tool-kit/jest": "^2.0.0", - "@dotcom-tool-kit/lint-staged": "^3.0.0", - "@dotcom-tool-kit/lint-staged-npm": "^2.0.0", - "@dotcom-tool-kit/mocha": "^2.0.0", - "@dotcom-tool-kit/n-test": "^2.0.0", - "@dotcom-tool-kit/next-router": "^2.0.0", - "@dotcom-tool-kit/nodemon": "^2.0.0", - "@dotcom-tool-kit/npm": "^2.0.0", - "@dotcom-tool-kit/pa11y": "^0.3.0", - "@dotcom-tool-kit/prettier": "^2.0.0", - "@dotcom-tool-kit/upload-assets-to-s3": "^2.0.0", - "dotcom-tool-kit": "^2.0.0", - "nodemon": "^2.0.15" + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", + "winston": "^3.11.0", + "zod": "^3.22.4" + }, + "peerDependencies": { + "zod": "^3.22.4" + } + }, + "lib/base/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" }, "engines": { - "node": "16.x || 18.x", - "npm": "7.x || 8.x || 9.x" + "node": ">=10" + } + }, + "lib/base/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "lib/base/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "lib/config": { + "name": "@dotcom-tool-kit/config", + "version": "2.0.0-beta.0", + "license": "ISC", + "dependencies": { + "@dotcom-tool-kit/conflict": "2.0.0-beta.0", + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", + "@dotcom-tool-kit/validated": "2.0.0-beta.0" + } + }, + "lib/conflict": { + "name": "@dotcom-tool-kit/conflict", + "version": "2.0.0-beta.0", + "license": "ISC", + "dependencies": { + "@dotcom-tool-kit/plugin": "2.0.0-beta.0" } }, "lib/doppler": { "name": "@dotcom-tool-kit/doppler", - "version": "1.1.0", + "version": "2.0.0-beta.0", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/options": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", - "@dotcom-tool-kit/vault": "^3.2.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/vault": "4.0.0-beta.0", "tslib": "^2.3.1" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "spawk": "^1.8.1", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } }, @@ -1127,13 +1201,13 @@ }, "lib/error": { "name": "@dotcom-tool-kit/error", - "version": "3.2.0", + "version": "4.0.0-beta.0", "license": "ISC", "dependencies": { "tslib": "^2.3.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } }, @@ -1144,10 +1218,11 @@ }, "lib/logger": { "name": "@dotcom-tool-kit/logger", - "version": "3.4.0", + "version": "4.0.0-beta.0", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", + "@apaleslimghost/boxen": "^5.1.3", + "@dotcom-tool-kit/error": "4.0.0-beta.0", "ansi-colors": "^4.1.1", "ansi-regex": "^5.0.1", "triple-beam": "^1.3.0", @@ -1159,7 +1234,7 @@ "@types/triple-beam": "^1.3.2" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } }, @@ -1170,14 +1245,14 @@ }, "lib/options": { "name": "@dotcom-tool-kit/options", - "version": "3.2.0", + "version": "4.0.0-beta.0", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "tslib": "^2.3.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } }, @@ -1189,6 +1264,7 @@ "lib/package-json-hook": { "name": "@dotcom-tool-kit/package-json-hook", "version": "4.2.0", + "extraneous": true, "license": "ISC", "dependencies": { "@financial-times/package-json": "^3.0.0", @@ -1205,20 +1281,34 @@ "npm": "7.x || 8.x || 9.x || 10.x" } }, - "lib/package-json-hook/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "lib/plugin": { + "name": "@dotcom-tool-kit/plugin", + "version": "2.0.0-beta.0", + "license": "ISC", + "devDependencies": {} + }, + "lib/schemas": { + "name": "@dotcom-tool-kit/schemas", + "version": "2.0.0-beta.0", + "license": "ISC", + "devDependencies": { + "prompts": "^2.4.2", + "winston": "^3.11.0", + "zod": "^3.22.4" + }, + "peerDependencies": { + "zod": "^3.22.4" + } }, "lib/state": { "name": "@dotcom-tool-kit/state", - "version": "3.2.0", + "version": "4.0.0-beta.0", "license": "ISC", "dependencies": { "tslib": "^2.3.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } }, @@ -1230,38 +1320,47 @@ "lib/types": { "name": "@dotcom-tool-kit/types", "version": "3.6.0", + "extraneous": true, "license": "ISC", "dependencies": { + "@dotcom-tool-kit/conflict": "^1.0.0", "@dotcom-tool-kit/error": "^3.2.0", "@dotcom-tool-kit/logger": "^3.4.0", + "@dotcom-tool-kit/validated": "^1.0.0", "semver": "^7.3.7", - "tslib": "^2.3.1", - "zod": "^3.20.2" + "tslib": "^2.3.1" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "^1.0.0", "@jest/globals": "^27.4.6", "@types/prompts": "^2.0.14", "@types/semver": "^7.3.9", - "winston": "^3.5.1" + "winston": "^3.5.1", + "zod": "^3.22.4" }, "engines": { "node": "16.x || 18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" + }, + "peerDependencies": { + "zod": "^3.22.4" } }, - "lib/types/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + "lib/validated": { + "name": "@dotcom-tool-kit/validated", + "version": "2.0.0-beta.0", + "license": "ISC", + "dependencies": { + "@dotcom-tool-kit/error": "4.0.0-beta.0" + } }, "lib/vault": { "name": "@dotcom-tool-kit/vault", - "version": "3.2.0", + "version": "4.0.0-beta.0", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/options": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/options": "4.0.0-beta.0", "@financial-times/n-fetch": "^1.0.0", "fs": "0.0.1-security", "os": "^0.1.2", @@ -1270,11 +1369,12 @@ "tslib": "^2.3.1" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@types/jest": "^27.0.2", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } }, @@ -1285,7 +1385,7 @@ }, "lib/wait-for-ok": { "name": "@dotcom-tool-kit/wait-for-ok", - "version": "3.2.0", + "version": "4.0.0-beta.0", "license": "ISC", "dependencies": { "node-fetch": "^2.6.8", @@ -1297,7 +1397,7 @@ "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } }, @@ -1334,6 +1434,38 @@ "node": ">=6.0.0" } }, + "node_modules/@apaleslimghost/boxen": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@apaleslimghost/boxen/-/boxen-5.1.3.tgz", + "integrity": "sha512-UkSSOihJUY2VKdU0WE8NH6xgB/P1iMEODkaXlRqCh5WDZ/8weZq/eHblFJM+F9CCd+QMkIbp2TjCmJB1A9rTRg==", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@apaleslimghost/boxen/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@aws-crypto/crc32": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-3.0.0.tgz", @@ -4224,38 +4356,45 @@ "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, "node_modules/@babel/code-frame": { - "version": "7.12.11", - "license": "MIT", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", "dependencies": { - "@babel/highlight": "^7.10.4" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.17.10", - "license": "MIT", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.17.10", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-compilation-targets": "^7.17.10", - "@babel/helper-module-transforms": "^7.17.7", - "@babel/helpers": "^7.17.9", - "@babel/parser": "^7.17.10", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.10", - "@babel/types": "^7.17.10", - "convert-source-map": "^1.7.0", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.4.tgz", + "integrity": "sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.4", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.4", + "@babel/parser": "^7.24.4", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" + "json5": "^2.2.3", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -4265,35 +4404,41 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/@babel/code-frame": { - "version": "7.16.7", - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "license": "ISC", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.17.10", - "license": "MIT", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", "dependencies": { - "@babel/types": "^7.17.10", - "@jridgewell/gen-mapping": "^0.1.0", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.16.7", "dev": true, @@ -4318,28 +4463,41 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.17.10", - "license": "MIT", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dependencies": { - "@babel/compat-data": "^7.17.10", - "@babel/helper-validator-option": "^7.16.7", - "browserslist": "^4.20.2", - "semver": "^6.3.0" + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "license": "ISC", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.17.9", "dev": true, @@ -4402,11 +4560,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.16.7", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.16.7" - }, + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } @@ -4423,21 +4579,23 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.17.9", - "license": "MIT", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.16.7", - "@babel/types": "^7.17.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.16.7", - "license": "MIT", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -4455,30 +4613,32 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.16.7", - "license": "MIT", + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.17.7", - "license": "MIT", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dependencies": { - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-module-imports": "^7.16.7", - "@babel/helper-simple-access": "^7.17.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/helper-validator-identifier": "^7.16.7", - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.3", - "@babel/types": "^7.17.0" + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/helper-validator-identifier": "^7.22.20" }, "engines": { "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, "node_modules/@babel/helper-optimise-call-expression": { @@ -4529,10 +4689,11 @@ } }, "node_modules/@babel/helper-simple-access": { - "version": "7.17.7", - "license": "MIT", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dependencies": { - "@babel/types": "^7.17.0" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -4550,26 +4711,36 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.16.7", - "license": "MIT", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/types": "^7.16.7" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.16.7", - "license": "MIT", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "engines": { "node": ">=6.9.0" } @@ -4589,25 +4760,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.17.9", - "license": "MIT", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.4.tgz", + "integrity": "sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==", "dependencies": { - "@babel/template": "^7.16.7", - "@babel/traverse": "^7.17.9", - "@babel/types": "^7.17.0" + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.1", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -4671,8 +4844,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.17.10", - "license": "MIT", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.4.tgz", + "integrity": "sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==", "bin": { "parser": "bin/babel-parser.js" }, @@ -5033,7 +5207,6 @@ "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dev": true, "dependencies": { "@babel/helper-plugin-utils": "^7.18.6" }, @@ -5748,56 +5921,38 @@ } }, "node_modules/@babel/template": { - "version": "7.16.7", - "license": "MIT", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/parser": "^7.16.7", - "@babel/types": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/code-frame": { - "version": "7.16.7", - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.16.7" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.17.10", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.10", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.17.9", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.10", - "@babel/types": "^7.17.10", - "debug": "^4.1.0", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", + "dependencies": { + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/@babel/code-frame": { - "version": "7.16.7", - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.16.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/traverse/node_modules/globals": { "version": "11.12.0", "license": "MIT", @@ -5806,10 +5961,12 @@ } }, "node_modules/@babel/types": { - "version": "7.17.10", - "license": "MIT", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dependencies": { - "@babel/helper-validator-identifier": "^7.16.7", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -5818,7 +5975,8 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" }, "node_modules/@colors/colors": { "version": "1.5.0", @@ -5951,6 +6109,19 @@ "node": ">=v12" } }, + "node_modules/@commitlint/load/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/@commitlint/message": { "version": "16.2.1", "dev": true, @@ -6058,7 +6229,7 @@ }, "node_modules/@cspotcode/source-map-consumer": { "version": "0.8.0", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">= 12" @@ -6066,7 +6237,7 @@ }, "node_modules/@cspotcode/source-map-support": { "version": "0.7.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-consumer": "0.8.0" @@ -6192,6 +6363,10 @@ "resolved": "plugins/backend-serverless-app", "link": true }, + "node_modules/@dotcom-tool-kit/base": { + "resolved": "lib/base", + "link": true + }, "node_modules/@dotcom-tool-kit/circleci": { "resolved": "plugins/circleci", "link": true @@ -6212,6 +6387,14 @@ "resolved": "plugins/component", "link": true }, + "node_modules/@dotcom-tool-kit/config": { + "resolved": "lib/config", + "link": true + }, + "node_modules/@dotcom-tool-kit/conflict": { + "resolved": "lib/conflict", + "link": true + }, "node_modules/@dotcom-tool-kit/create": { "resolved": "core/create", "link": true @@ -6293,13 +6476,25 @@ "link": true }, "node_modules/@dotcom-tool-kit/package-json-hook": { - "resolved": "lib/package-json-hook", + "resolved": "plugins/package-json-hook", + "link": true + }, + "node_modules/@dotcom-tool-kit/parallel": { + "resolved": "plugins/parallel", + "link": true + }, + "node_modules/@dotcom-tool-kit/plugin": { + "resolved": "lib/plugin", "link": true }, "node_modules/@dotcom-tool-kit/prettier": { "resolved": "plugins/prettier", "link": true }, + "node_modules/@dotcom-tool-kit/schemas": { + "resolved": "lib/schemas", + "link": true + }, "node_modules/@dotcom-tool-kit/serverless": { "resolved": "plugins/serverless", "link": true @@ -6308,9 +6503,28 @@ "resolved": "lib/state", "link": true }, + "node_modules/@dotcom-tool-kit/task": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@dotcom-tool-kit/task/-/task-1.2.0.tgz", + "integrity": "sha512-mup2OLklxy5ecw1NsU74PYMk68SPzrbwsyozVMFJEk+rqVbiXheQzTEuOxdXga7ewPSWFajR2A0+Pq5R2TnQHQ==", + "dependencies": { + "@dotcom-tool-kit/types": "^1.2.0" + } + }, "node_modules/@dotcom-tool-kit/types": { - "resolved": "lib/types", - "link": true + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@dotcom-tool-kit/types/-/types-1.9.0.tgz", + "integrity": "sha512-tEY8FbBHeQXzWnSz1WesvhxdECDWrmHey611WcvAtfzgopE1smztUyfziIyEtYfBp08EsB4L/HKq++5QTSOe8w==", + "dependencies": { + "@dotcom-tool-kit/error": "^1.9.0", + "lodash.isplainobject": "^4.0.6", + "lodash.mapvalues": "^4.6.0" + } + }, + "node_modules/@dotcom-tool-kit/types/node_modules/@dotcom-tool-kit/error": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@dotcom-tool-kit/error/-/error-1.9.0.tgz", + "integrity": "sha512-Jzc3Xu1S4gwNuRfs8+dqAieVAIdfX4cvOWN+morVtjy2bVSHmuV+2Qg6uAHWDeOtrlrACG8NwiSXan1UdPoj7A==" }, "node_modules/@dotcom-tool-kit/typescript": { "resolved": "plugins/typescript", @@ -6320,6 +6534,10 @@ "resolved": "plugins/upload-assets-to-s3", "link": true }, + "node_modules/@dotcom-tool-kit/validated": { + "resolved": "lib/validated", + "link": true + }, "node_modules/@dotcom-tool-kit/vault": { "resolved": "lib/vault", "link": true @@ -6332,159 +6550,527 @@ "resolved": "plugins/webpack", "link": true }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=12" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", - "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=12" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=12" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", - "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.1", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=12" } }, - "node_modules/@eslint/js": { - "version": "8.37.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.37.0.tgz", - "integrity": "sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=12" } }, - "node_modules/@financial-times/eslint-config-next": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@financial-times/eslint-config-next/-/eslint-config-next-6.0.0.tgz", - "integrity": "sha512-8zQ4c4I11CTahJx4MI/WWT/xKSqubRAlHm37IyrqrO9KSA9+KB6iyMj5vF8WttxNB1M8sIWHTgqZwoJmgnHhxQ==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], "dev": true, - "hasInstallScript": true, - "dependencies": { - "eslint": ">=5.0.0", - "eslint-plugin-no-only-tests": ">=2.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "14.x || 16.x", - "npm": "7.x || 8.x" + "node": ">=12" } }, - "node_modules/@financial-times/n-fetch": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@financial-times/n-fetch/-/n-fetch-1.0.0.tgz", - "integrity": "sha512-6ayYnrlSJZvGU8/ZJEskxYH2kdvwJU/aL9MSXc3FmPj3dft7ecMxsB4p8PsWeJamHyd8EjaL7Mwm63LR2DwePg==", - "dependencies": { - "@dotcom-reliability-kit/logger": "^2.2.7", - "http-errors": "^1.6.1", - "node-fetch": "^2.0.0" - }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "16.x || 18.x", - "npm": "7.x || 8.x || 9.x" + "node": ">=12" } }, - "node_modules/@financial-times/package-json": { - "version": "3.0.0", - "license": "MIT" - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "license": "MIT" + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@google-automations/git-file-utils": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@google-automations/git-file-utils/-/git-file-utils-1.2.3.tgz", - "integrity": "sha512-CdxeRDIExWMdMIhRNbbqI3amtF72jct3XVEKquqinkSbUbzYx+ZxvakCwJB0FDS8xmq9mjEWEt0Cxy/+49A8kw==", + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "@octokit/rest": "19.0.5", - "@octokit/types": "^8.0.0", - "minimatch": "^5.1.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">= 14" + "node": ">=12" } }, - "node_modules/@google-automations/git-file-utils/node_modules/@octokit/openapi-types": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-14.0.0.tgz", - "integrity": "sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw==", - "dev": true + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } }, - "node_modules/@google-automations/git-file-utils/node_modules/@octokit/types": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-8.0.0.tgz", - "integrity": "sha512-65/TPpOJP1i3K4lBJMnWqPUJ6zuOtzhtagDvydAWbEXpbFYA0oMKKyLb95NFZZP0lSh/4b6K+DQlzvYQJQQePg==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "@octokit/openapi-types": "^14.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@google-automations/git-file-utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@google-automations/git-file-utils/node_modules/minimatch": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", - "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", + "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz", + "integrity": "sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.1", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.37.0.tgz", + "integrity": "sha512-x5vzdtOOGgFVDCUs81QRB2+liax8rFg3+7hqM+QhBG0/G3F1ZsoYl97UrqgHgQ9KKT7G6c4V+aTUCgu/n22v1A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@financial-times/eslint-config-next": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@financial-times/eslint-config-next/-/eslint-config-next-6.0.0.tgz", + "integrity": "sha512-8zQ4c4I11CTahJx4MI/WWT/xKSqubRAlHm37IyrqrO9KSA9+KB6iyMj5vF8WttxNB1M8sIWHTgqZwoJmgnHhxQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "eslint": ">=5.0.0", + "eslint-plugin-no-only-tests": ">=2.0.0" + }, + "engines": { + "node": "14.x || 16.x", + "npm": "7.x || 8.x" + } + }, + "node_modules/@financial-times/n-fetch": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@financial-times/n-fetch/-/n-fetch-1.0.0.tgz", + "integrity": "sha512-6ayYnrlSJZvGU8/ZJEskxYH2kdvwJU/aL9MSXc3FmPj3dft7ecMxsB4p8PsWeJamHyd8EjaL7Mwm63LR2DwePg==", + "dependencies": { + "@dotcom-reliability-kit/logger": "^2.2.7", + "http-errors": "^1.6.1", + "node-fetch": "^2.0.0" + }, + "engines": { + "node": "16.x || 18.x", + "npm": "7.x || 8.x || 9.x" + } + }, + "node_modules/@financial-times/package-json": { + "version": "3.0.0", + "license": "MIT" + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "license": "MIT" + }, + "node_modules/@google-automations/git-file-utils": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@google-automations/git-file-utils/-/git-file-utils-1.2.3.tgz", + "integrity": "sha512-CdxeRDIExWMdMIhRNbbqI3amtF72jct3XVEKquqinkSbUbzYx+ZxvakCwJB0FDS8xmq9mjEWEt0Cxy/+49A8kw==", + "dev": true, + "dependencies": { + "@octokit/rest": "19.0.5", + "@octokit/types": "^8.0.0", + "minimatch": "^5.1.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@google-automations/git-file-utils/node_modules/@octokit/openapi-types": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-14.0.0.tgz", + "integrity": "sha512-HNWisMYlR8VCnNurDU6os2ikx0s0VyEjDYHNS/h4cgb8DeOxQ0n72HyinUtdDVxJhFy3FWLGl0DJhfEWk3P5Iw==", + "dev": true + }, + "node_modules/@google-automations/git-file-utils/node_modules/@octokit/types": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-8.0.0.tgz", + "integrity": "sha512-65/TPpOJP1i3K4lBJMnWqPUJ6zuOtzhtagDvydAWbEXpbFYA0oMKKyLb95NFZZP0lSh/4b6K+DQlzvYQJQQePg==", + "dev": true, + "dependencies": { + "@octokit/openapi-types": "^14.0.0" + } + }, + "node_modules/@google-automations/git-file-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@google-automations/git-file-utils/node_modules/minimatch": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.1.tgz", + "integrity": "sha512-362NP+zlprccbEt/SkxKfRMHnNY85V74mVnpUpNyr3F35covl09Kec7/sEFLt3RA4oXmewtoaanoIf67SE5Y5g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" } }, "node_modules/@hapi/accept": { @@ -6945,55 +7531,135 @@ } }, "node_modules/@jest/console": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dependencies": { - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", "slash": "^3.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/core": { - "version": "27.5.1", - "license": "MIT", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/reporters": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "emittery": "^0.8.1", + "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^27.5.1", - "jest-config": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-resolve-dependencies": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "jest-watcher": "^27.5.1", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", "micromatch": "^4.0.4", - "rimraf": "^3.0.0", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -7004,34 +7670,59 @@ } } }, - "node_modules/@jest/core/node_modules/acorn": { - "version": "8.7.1", - "license": "MIT", + "node_modules/@jest/core/node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "optional": true, "peer": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { - "node": ">=0.4.0" + "node": ">=12" } }, - "node_modules/@jest/core/node_modules/acorn-walk": { - "version": "8.2.0", - "license": "MIT", + "node_modules/@jest/core/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "optional": true, "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "engines": { - "node": ">=0.4.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@jest/core/node_modules/ci-info": { - "version": "3.3.0", - "license": "MIT" + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } }, "node_modules/@jest/core/node_modules/diff": { "version": "4.0.2", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "optional": true, "peer": true, "engines": { @@ -7039,53 +7730,118 @@ } }, "node_modules/@jest/core/node_modules/jest-config": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dependencies": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.5.1", - "@jest/types": "^27.5.1", - "babel-jest": "^27.5.1", + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", - "glob": "^7.1.1", + "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-jasmine2": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^27.5.1", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { + "@types/node": "*", "ts-node": ">=9.0.0" }, "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, "ts-node": { "optional": true } } }, + "node_modules/@jest/core/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/@jest/core/node_modules/ts-node": { - "version": "10.7.0", - "license": "MIT", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "optional": true, "peer": true, "dependencies": { - "@cspotcode/source-map-support": "0.7.0", + "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -7096,7 +7852,7 @@ "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", + "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "bin": { @@ -7124,6 +7880,7 @@ }, "node_modules/@jest/environment": { "version": "27.5.1", + "dev": true, "license": "MIT", "dependencies": { "@jest/fake-timers": "^27.5.1", @@ -7135,109 +7892,57 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/expect": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.3.1.tgz", - "integrity": "sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg==", + "node_modules/@jest/environment/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", "dev": true, "dependencies": { - "expect": "^29.3.1", - "jest-snapshot": "^29.3.1" + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/expect-utils": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.3.1.tgz", - "integrity": "sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==", - "dev": true, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dependencies": { - "jest-get-type": "^29.2.0" + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/expect-utils/node_modules/jest-get-type": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", - "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect/node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@jest/expect/node_modules/@jest/transform": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.3.1.tgz", - "integrity": "sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug==", - "dev": true, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.3.1", - "@jridgewell/trace-mapping": "^0.3.15", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.3.1", - "jest-regex-util": "^29.2.0", - "jest-util": "^29.3.1", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" + "jest-get-type": "^29.6.3" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/expect/node_modules/@jest/types": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.3.1.tgz", - "integrity": "sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.0.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, + "node_modules/@jest/expect-utils/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/expect/node_modules/@types/yargs": { - "version": "17.0.17", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.17.tgz", - "integrity": "sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, "node_modules/@jest/expect/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "engines": { "node": ">=10" }, @@ -7246,122 +7951,90 @@ } }, "node_modules/@jest/expect/node_modules/ci-info": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", - "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==", - "dev": true, + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "engines": { "node": ">=8" } }, - "node_modules/@jest/expect/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, "node_modules/@jest/expect/node_modules/diff-sequences": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.3.1.tgz", - "integrity": "sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==", - "dev": true, + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect/node_modules/expect": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.3.1.tgz", - "integrity": "sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dependencies": { - "@jest/expect-utils": "^29.3.1", - "jest-get-type": "^29.2.0", - "jest-matcher-utils": "^29.3.1", - "jest-message-util": "^29.3.1", - "jest-util": "^29.3.1" + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect/node_modules/jest-diff": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.3.1.tgz", - "integrity": "sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.3.1", - "jest-get-type": "^29.2.0", - "pretty-format": "^29.3.1" + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect/node_modules/jest-get-type": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", - "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", - "dev": true, + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/expect/node_modules/jest-haste-map": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.3.1.tgz", - "integrity": "sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A==", - "dev": true, - "dependencies": { - "@jest/types": "^29.3.1", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.2.0", - "jest-util": "^29.3.1", - "jest-worker": "^29.3.1", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "node_modules/@jest/expect/node_modules/jest-matcher-utils": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.3.1.tgz", - "integrity": "sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ==", - "dev": true, + "node_modules/@jest/expect/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.3.1", - "jest-get-type": "^29.2.0", - "pretty-format": "^29.3.1" + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect/node_modules/jest-message-util": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.3.1.tgz", - "integrity": "sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.3.1", + "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.3.1", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -7369,57 +8042,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/expect/node_modules/jest-regex-util": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.2.0.tgz", - "integrity": "sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/expect/node_modules/jest-snapshot": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.3.1.tgz", - "integrity": "sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.3.1", - "@jest/transform": "^29.3.1", - "@jest/types": "^29.3.1", - "@types/babel__traverse": "^7.0.6", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.3.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.3.1", - "jest-get-type": "^29.2.0", - "jest-haste-map": "^29.3.1", - "jest-matcher-utils": "^29.3.1", - "jest-message-util": "^29.3.1", - "jest-util": "^29.3.1", - "natural-compare": "^1.4.0", - "pretty-format": "^29.3.1", - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jest/expect/node_modules/jest-util": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.3.1.tgz", - "integrity": "sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dependencies": { - "@jest/types": "^29.3.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -7430,28 +8058,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/expect/node_modules/jest-worker": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.3.1.tgz", - "integrity": "sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==", - "dev": true, - "dependencies": { - "@types/node": "*", - "jest-util": "^29.3.1", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jest/expect/node_modules/pretty-format": { - "version": "29.3.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", - "integrity": "sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==", - "dev": true, + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dependencies": { - "@jest/schemas": "^29.0.0", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" }, @@ -7462,96 +8074,101 @@ "node_modules/@jest/expect/node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, - "node_modules/@jest/expect/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/@jest/fake-timers": { + "version": "27.5.1", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@jest/types": "^27.5.1", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.5.1", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/expect/node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "node_modules/@jest/fake-timers/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", "dev": true, "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/fake-timers": { + "node_modules/@jest/globals": { "version": "27.5.1", + "dev": true, "license": "MIT", "dependencies": { + "@jest/environment": "^27.5.1", "@jest/types": "^27.5.1", - "@sinonjs/fake-timers": "^8.0.1", - "@types/node": "*", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" + "expect": "^27.5.1" }, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/@jest/globals": { + "node_modules/@jest/globals/node_modules/@jest/types": { "version": "27.5.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/types": "^27.5.1", - "expect": "^27.5.1" + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" }, "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, "node_modules/@jest/reporters": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", "@types/node": "*", "chalk": "^4.0.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", - "glob": "^7.1.2", + "glob": "^7.1.3", "graceful-fs": "^4.2.9", "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-instrument": "^6.0.0", "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-haste-map": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "slash": "^3.0.0", - "source-map": "^0.6.0", "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^8.1.0" + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -7562,142 +8179,321 @@ } } }, - "node_modules/@jest/schemas": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", - "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.24.1" - }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "27.5.1", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9", - "source-map": "^0.6.0" + "node": ">=10" }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/@jest/test-result": { - "version": "27.5.1", - "license": "MIT", - "dependencies": { - "@jest/console": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - }, + "node_modules/@jest/reporters/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": ">=8" } }, - "node_modules/@jest/test-sequencer": { - "version": "27.5.1", - "license": "MIT", + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", + "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", "dependencies": { - "@jest/test-result": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-runtime": "^27.5.1" + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": ">=10" } }, - "node_modules/@jest/transform": { - "version": "27.5.1", - "license": "MIT", + "node_modules/@jest/reporters/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dependencies": { - "@babel/core": "^7.1.0", - "@jest/types": "^27.5.1", - "babel-plugin-istanbul": "^6.1.1", + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-util": "^27.5.1", "micromatch": "^4.0.4", - "pirates": "^4.0.4", + "pretty-format": "^29.7.0", "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" + "stack-utils": "^2.0.3" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/types": { - "version": "27.5.1", - "license": "MIT", + "node_modules/@jest/reporters/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dependencies": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", + "@jest/types": "^29.6.3", "@types/node": "*", - "@types/yargs": "^16.0.0", - "chalk": "^4.0.0" + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "license": "MIT", + "node_modules/@jest/reporters/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" + "yallist": "^4.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">=10" } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "node_modules/@jest/reporters/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, "engines": { - "node": ">=6.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.0", - "license": "MIT", + "node_modules/@jest/reporters/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/@jest/reporters/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, "engines": { - "node": ">=6.0.0" + "node": ">=10" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "node_modules/@jest/reporters/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@kwsites/file-exists": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", - "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dependencies": { - "debug": "^4.1.1" - } - }, + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dependencies": { + "debug": "^4.1.1" + } + }, "node_modules/@kwsites/promise-deferred": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", @@ -9262,10 +10058,9 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", - "dev": true + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" }, "node_modules/@sindresorhus/is": { "version": "0.14.0", @@ -9277,6 +10072,7 @@ }, "node_modules/@sinonjs/commons": { "version": "1.8.3", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" @@ -9284,6 +10080,7 @@ }, "node_modules/@sinonjs/fake-timers": { "version": "8.1.0", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^1.7.0" @@ -9335,26 +10132,29 @@ "devOptional": true }, "node_modules/@types/babel__core": { - "version": "7.1.19", - "license": "MIT", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "node_modules/@types/babel__generator": { - "version": "7.6.4", - "license": "MIT", + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { - "version": "7.4.1", - "license": "MIT", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -9478,15 +10278,6 @@ "@types/node": "*" } }, - "node_modules/@types/libnpmpublish": { - "version": "4.0.1", - "dev": true, - "dependencies": { - "@npm/types": "*", - "@types/node-fetch": "*", - "@types/npm-registry-fetch": "*" - } - }, "node_modules/@types/lodash": { "version": "4.14.189", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.189.tgz", @@ -9593,10 +10384,6 @@ "version": "4.0.0", "license": "MIT" }, - "node_modules/@types/prettier": { - "version": "2.6.0", - "license": "MIT" - }, "node_modules/@types/prompts": { "version": "2.0.14", "dev": true, @@ -9674,21 +10461,13 @@ "@types/node": "*" } }, - "node_modules/@types/tar": { - "version": "6.1.1", + "node_modules/@types/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@types/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-+VfWIwrlept2VBTj7Y2wQnI/Xfscy1u8Pyj/puYwss6V1IblXn1x7S0S9eFh6KyBolgLCm+rUFzhFAbdkR691g==", "dev": true, "dependencies": { - "@types/node": "*", - "minipass": "^4.0.0" - } - }, - "node_modules/@types/tar/node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "dev": true, - "engines": { - "node": ">=8" + "@types/node": "*" } }, "node_modules/@types/tough-cookie": { @@ -10190,10 +10969,6 @@ "es5-ext": "^0.10.47" } }, - "node_modules/abab": { - "version": "2.0.6", - "license": "BSD-3-Clause" - }, "node_modules/abbrev": { "version": "1.1.1", "license": "ISC" @@ -10210,8 +10985,10 @@ } }, "node_modules/acorn": { - "version": "7.4.1", - "license": "MIT", + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -10219,14 +10996,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-globals": { - "version": "6.0.0", - "license": "MIT", - "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "dev": true, @@ -10236,8 +11005,10 @@ } }, "node_modules/acorn-walk": { - "version": "7.2.0", - "license": "MIT", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "devOptional": true, "engines": { "node": ">=0.4.0" } @@ -10354,7 +11125,6 @@ "node_modules/ansi-align": { "version": "3.0.1", "license": "ISC", - "peer": true, "dependencies": { "string-width": "^4.1.0" } @@ -10544,12 +11314,15 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10647,16 +11420,17 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -10806,9 +11580,12 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -10919,20 +11696,20 @@ } }, "node_modules/babel-jest": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dependencies": { - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^27.5.1", + "babel-preset-jest": "^29.6.3", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.8.0" @@ -10961,16 +11738,17 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "27.5.1", - "license": "MIT", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", + "@types/babel__core": "^7.1.14", "@types/babel__traverse": "^7.0.6" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/babel-plugin-polyfill-corejs2": { @@ -11039,14 +11817,15 @@ } }, "node_modules/babel-preset-jest": { - "version": "27.5.1", - "license": "MIT", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dependencies": { - "babel-plugin-jest-hoist": "^27.5.1", + "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "@babel/core": "^7.0.0" @@ -11230,7 +12009,8 @@ }, "node_modules/boxen": { "version": "5.1.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", + "integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==", "peer": true, "dependencies": { "ansi-align": "^3.0.0", @@ -11282,10 +12062,6 @@ "version": "1.1.0", "license": "MIT" }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "license": "BSD-2-Clause" - }, "node_modules/browser-stdout": { "version": "1.3.1", "license": "ISC", @@ -11371,7 +12147,9 @@ } }, "node_modules/browserslist": { - "version": "4.20.3", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "funding": [ { "type": "opencollective", @@ -11380,15 +12158,17 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001332", - "electron-to-chromium": "^1.4.118", - "escalade": "^3.1.1", - "node-releases": "^2.0.3", - "picocolors": "^1.0.0" + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -11481,6 +12261,21 @@ "version": "1.0.3", "license": "MIT" }, + "node_modules/bundle-require": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-4.0.3.tgz", + "integrity": "sha512-2iscZ3fcthP2vka4Y7j277YJevwmsby/FpFDwjgw34Nl7dtCpt7zz/4TexmHMzY6KZEih7En9ImlbbgUNNQGtA==", + "dev": true, + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.17" + } + }, "node_modules/cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -11625,13 +12420,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -11668,7 +12468,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001336", + "version": "1.0.30001612", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz", + "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==", "funding": [ { "type": "opencollective", @@ -11677,9 +12479,12 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/caseless": { "version": "0.12.0", @@ -11924,7 +12729,6 @@ "node_modules/cli-boxes": { "version": "2.2.1", "license": "MIT", - "peer": true, "engines": { "node": ">=6" }, @@ -12554,11 +13358,9 @@ } }, "node_modules/convert-source-map": { - "version": "1.8.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.1" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/cookiejar": { "version": "2.1.4", @@ -12666,31 +13468,12 @@ "typescript": ">=3" } }, - "node_modules/cosmiconfig-typescript-loader/node_modules/acorn": { - "version": "8.7.1", + "node_modules/cosmiconfig-typescript-loader/node_modules/diff": { + "version": "4.0.2", "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/cosmiconfig-typescript-loader/node_modules/acorn-walk": { - "version": "8.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/cosmiconfig-typescript-loader/node_modules/diff": { - "version": "4.0.2", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" + "node": ">=0.3.1" } }, "node_modules/cosmiconfig-typescript-loader/node_modules/ts-node": { @@ -12822,6 +13605,215 @@ "sha.js": "^2.4.8" } }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/create-jest/node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/create-jest/node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/create-jest/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/create-jest/node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "optional": true, + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/create-require": { "version": "1.1.1", "devOptional": true, @@ -12951,24 +13943,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/cssom": { - "version": "0.4.4", - "license": "MIT" - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "license": "MIT" - }, "node_modules/cwise-compiler": { "version": "1.1.3", "license": "MIT", @@ -13030,16 +14004,52 @@ "version": "0.0.3", "license": "MIT" }, - "node_modules/data-urls": { - "version": "2.0.0", - "license": "MIT", + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/dateformat": { @@ -13099,10 +14109,6 @@ "node": ">=0.10.0" } }, - "node_modules/decimal.js": { - "version": "10.3.1", - "license": "MIT" - }, "node_modules/decode-uri-component": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", @@ -13432,16 +14438,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -13529,7 +14538,8 @@ }, "node_modules/detect-newline": { "version": "3.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "engines": { "node": ">=8" } @@ -13558,6 +14568,7 @@ }, "node_modules/diff-sequences": { "version": "27.5.1", + "dev": true, "license": "MIT", "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" @@ -13632,23 +14643,6 @@ ], "license": "BSD-2-Clause" }, - "node_modules/domexception": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "webidl-conversions": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/domexception/node_modules/webidl-conversions": { - "version": "5.0.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=8" - } - }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -13777,8 +14771,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.4.134", - "license": "ISC" + "version": "1.4.747", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.747.tgz", + "integrity": "sha512-+FnSWZIAvFHbsNVmUxhEqWiaOiPMcfum1GQzlWCg/wLigVtshOsjXHyEFfmt6cFK6+HkS3QOJBv6/3OPumbBfw==" }, "node_modules/elliptic": { "version": "6.5.4", @@ -13798,10 +14793,11 @@ "license": "MIT" }, "node_modules/emittery": { - "version": "0.8.1", - "license": "MIT", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sindresorhus/emittery?sponsor=1" @@ -13843,6 +14839,16 @@ "once": "^1.4.0" } }, + "node_modules/endent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/endent/-/endent-2.1.0.tgz", + "integrity": "sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w==", + "dependencies": { + "dedent": "^0.7.0", + "fast-json-parse": "^1.0.3", + "objectorarray": "^1.0.5" + } + }, "node_modules/enhanced-resolve": { "version": "4.5.0", "dependencies": { @@ -13945,49 +14951,56 @@ } }, "node_modules/es-abstract": { - "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "arraybuffer.prototype.slice": "^1.0.2", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.5", - "es-set-tostringtag": "^2.0.1", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.2", - "get-symbol-description": "^1.0.0", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", "has-symbols": "^1.0.3", - "hasown": "^2.0.0", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", + "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", - "is-typed-array": "^1.1.12", + "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "safe-array-concat": "^1.0.1", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.8", - "string.prototype.trimend": "^1.0.7", - "string.prototype.trimstart": "^1.0.7", - "typed-array-buffer": "^1.0.0", - "typed-array-byte-length": "^1.0.0", - "typed-array-byte-offset": "^1.0.0", - "typed-array-length": "^1.0.4", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.13" + "which-typed-array": "^1.1.15" }, "engines": { "node": ">= 0.4" @@ -13996,22 +15009,52 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" } }, - "node_modules/es-shim-unscopables": { + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", "dev": true, "dependencies": { @@ -14102,6 +15145,44 @@ "es6-symbol": "^3.1.1" } }, + "node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, "node_modules/escalade": { "version": "3.1.1", "license": "MIT", @@ -14131,75 +15212,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.0.0", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "5.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/escodegen/node_modules/levn": { - "version": "0.3.0", - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/optionator": { - "version": "0.8.3", - "license": "MIT", - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/prelude-ls": { - "version": "1.1.2", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/type-check": { - "version": "0.3.2", - "license": "MIT", - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint": { "version": "8.37.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.37.0.tgz", @@ -14538,18 +15550,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree/node_modules/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/espree/node_modules/eslint-visitor-keys": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", @@ -14628,6 +15628,7 @@ }, "node_modules/esutils": { "version": "2.0.3", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -14768,6 +15769,7 @@ }, "node_modules/expect": { "version": "27.5.1", + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^27.5.1", @@ -14779,6 +15781,22 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/expect/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -14978,12 +15996,18 @@ "node": ">=8.6.0" } }, + "node_modules/fast-json-parse": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fast-json-parse/-/fast-json-parse-1.0.3.tgz", + "integrity": "sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==" + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "dev": true, "license": "MIT" }, "node_modules/fast-redact": { @@ -15674,15 +16698,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -15748,11 +16776,13 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.0", - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -16018,19 +17048,20 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", "engines": { "node": ">= 0.4" }, @@ -16049,10 +17080,11 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "license": "MIT", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -16165,9 +17197,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dependencies": { "function-bind": "^1.1.2" }, @@ -16351,19 +17383,10 @@ "node": ">=6" } }, - "node_modules/html-encoding-sniffer": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^1.0.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/html-escaper": { "version": "2.0.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" }, "node_modules/htmlparser2": { "version": "8.0.1", @@ -16793,11 +17816,11 @@ } }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", "dependencies": { - "get-intrinsic": "^1.2.2", + "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" }, @@ -16871,13 +17894,15 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -16978,6 +18003,20 @@ "node": ">=0.10.0" } }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-date-object": { "version": "1.0.5", "license": "MIT", @@ -17135,8 +18174,9 @@ "peer": true }, "node_modules/is-negative-zero": { - "version": "2.0.2", - "license": "MIT", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "engines": { "node": ">= 0.4" }, @@ -17203,10 +18243,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "license": "MIT" - }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -17215,7 +18251,8 @@ }, "node_modules/is-regex": { "version": "1.1.4", - "license": "MIT", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -17242,10 +18279,14 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "license": "MIT", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -17299,11 +18340,11 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -17451,20 +18492,66 @@ } }, "node_modules/istanbul-lib-report": { - "version": "3.0.0", - "license": "BSD-3-Clause", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dependencies": { "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", + "make-dir": "^4.0.0", "supports-color": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, + "node_modules/istanbul-lib-report/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -17475,8 +18562,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.4", - "license": "BSD-3-Clause", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -17495,19 +18583,21 @@ } }, "node_modules/jest": { - "version": "27.5.1", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, - "license": "MIT", "dependencies": { - "@jest/core": "^27.5.1", + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", "import-local": "^3.0.2", - "jest-cli": "^27.5.1" + "jest-cli": "^29.7.0" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -17519,67 +18609,294 @@ } }, "node_modules/jest-changed-files": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dependencies": { - "@jest/types": "^27.5.1", "execa": "^5.0.0", - "throat": "^6.0.1" + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus": { - "version": "27.5.1", - "license": "MIT", + "node_modules/jest-changed-files/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.3", - "throat": "^6.0.1" + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/jest-circus/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/jest-circus/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-circus/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/jest-cli": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dependencies": { - "@jest/core": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", + "create-jest": "^29.7.0", "exit": "^0.1.2", - "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", - "prompts": "^2.0.1", - "yargs": "^16.2.0" + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" }, "bin": { "jest": "bin/jest.js" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" @@ -17590,34 +18907,59 @@ } } }, - "node_modules/jest-cli/node_modules/acorn": { - "version": "8.7.1", - "license": "MIT", + "node_modules/jest-cli/node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "optional": true, "peer": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" }, "engines": { - "node": ">=0.4.0" + "node": ">=12" } }, - "node_modules/jest-cli/node_modules/acorn-walk": { - "version": "8.2.0", - "license": "MIT", + "node_modules/jest-cli/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", "optional": true, "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "engines": { - "node": ">=0.4.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-cli/node_modules/ci-info": { - "version": "3.3.0", - "license": "MIT" + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } }, "node_modules/jest-cli/node_modules/diff": { "version": "4.0.2", - "license": "BSD-3-Clause", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "optional": true, "peer": true, "engines": { @@ -17625,53 +18967,99 @@ } }, "node_modules/jest-cli/node_modules/jest-config": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dependencies": { - "@babel/core": "^7.8.0", - "@jest/test-sequencer": "^27.5.1", - "@jest/types": "^27.5.1", - "babel-jest": "^27.5.1", + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", - "glob": "^7.1.1", + "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-jasmine2": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runner": "^27.5.1", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^27.5.1", + "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { + "@types/node": "*", "ts-node": ">=9.0.0" }, "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, "ts-node": { "optional": true } } }, + "node_modules/jest-cli/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-cli/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/jest-cli/node_modules/ts-node": { - "version": "10.7.0", - "license": "MIT", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "optional": true, "peer": true, "dependencies": { - "@cspotcode/source-map-support": "0.7.0", + "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", "@tsconfig/node12": "^1.0.7", "@tsconfig/node14": "^1.0.0", @@ -17682,7 +19070,7 @@ "create-require": "^1.1.0", "diff": "^4.0.1", "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.0", + "v8-compile-cache-lib": "^3.0.1", "yn": "3.1.1" }, "bin": { @@ -17708,24 +19096,9 @@ } } }, - "node_modules/jest-cli/node_modules/yargs": { - "version": "16.2.0", - "license": "MIT", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-diff": { "version": "27.5.1", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -17738,130 +19111,365 @@ } }, "node_modules/jest-docblock": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dependencies": { "detect-newline": "^3.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-each": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dependencies": { - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1" + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-environment-jsdom": { - "version": "27.5.1", - "license": "MIT", - "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1", - "jsdom": "^16.6.0" - }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "27.5.1", - "license": "MIT", + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1" + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/jest-environment-node/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-node/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-node/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-environment-node/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/jest-get-type": { "version": "27.5.1", + "dev": true, "license": "MIT", "engines": { "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, "node_modules/jest-haste-map": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dependencies": { - "@jest/types": "^27.5.1", - "@types/graceful-fs": "^4.1.2", + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^27.5.1", - "jest-serializer": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", "micromatch": "^4.0.4", - "walker": "^1.0.7" + "walker": "^1.0.8" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, - "node_modules/jest-jasmine2": { - "version": "27.5.1", - "license": "MIT", + "node_modules/jest-haste-map/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^27.5.1", - "is-generator-fn": "^2.0.0", - "jest-each": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "pretty-format": "^27.5.1", - "throat": "^6.0.1" + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-leak-detector": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dependencies": { - "jest-get-type": "^27.5.1", - "pretty-format": "^27.5.1" + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-leak-detector/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/jest-matcher-utils": { "version": "27.5.1", + "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.0.0", @@ -17875,6 +19483,7 @@ }, "node_modules/jest-message-util": { "version": "27.5.1", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", @@ -17891,18 +19500,25 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/jest-message-util/node_modules/@babel/code-frame": { - "version": "7.16.7", - "license": "MIT", + "node_modules/jest-message-util/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, "dependencies": { - "@babel/highlight": "^7.16.7" + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" }, "engines": { - "node": ">=6.9.0" + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, "node_modules/jest-mock": { "version": "27.5.1", + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^27.5.1", @@ -17912,6 +19528,22 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/jest-mock/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/jest-pnp-resolver": { "version": "1.2.2", "license": "MIT", @@ -17928,148 +19560,634 @@ } }, "node_modules/jest-regex-util": { - "version": "27.5.1", - "license": "MIT", + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dependencies": { - "@jest/types": "^27.5.1", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", + "jest-haste-map": "^29.7.0", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^27.5.1", - "jest-validate": "^27.5.1", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", "resolve": "^1.20.0", - "resolve.exports": "^1.1.0", + "resolve.exports": "^2.0.0", "slash": "^3.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve-dependencies": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/jest-runner/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/jest-runtime/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dependencies": { - "@jest/types": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-snapshot": "^27.5.1" + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runner": { - "version": "27.5.1", - "license": "MIT", + "node_modules/jest-snapshot/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dependencies": { - "@jest/console": "^27.5.1", - "@jest/environment": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", - "emittery": "^0.8.1", + "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", - "jest-docblock": "^27.5.1", - "jest-environment-jsdom": "^27.5.1", - "jest-environment-node": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-leak-detector": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-runtime": "^27.5.1", - "jest-util": "^27.5.1", - "jest-worker": "^27.5.1", - "source-map-support": "^0.5.6", - "throat": "^6.0.1" + "picomatch": "^2.2.3" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runtime": { - "version": "27.5.1", - "license": "MIT", + "node_modules/jest-snapshot/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/globals": "^27.5.1", - "@jest/source-map": "^27.5.1", - "@jest/test-result": "^27.5.1", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "execa": "^5.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-mock": "^27.5.1", - "jest-regex-util": "^27.5.1", - "jest-resolve": "^27.5.1", - "jest-snapshot": "^27.5.1", - "jest-util": "^27.5.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" + "yallist": "^4.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": ">=10" } }, - "node_modules/jest-serializer": { - "version": "27.5.1", - "license": "MIT", + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dependencies": { - "@types/node": "*", - "graceful-fs": "^4.2.9" + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot": { - "version": "27.5.1", - "license": "MIT", + "node_modules/jest-snapshot/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dependencies": { - "@babel/core": "^7.7.2", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/traverse": "^7.7.2", - "@babel/types": "^7.0.0", - "@jest/transform": "^27.5.1", - "@jest/types": "^27.5.1", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.1.5", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^27.5.1", - "graceful-fs": "^4.2.9", - "jest-diff": "^27.5.1", - "jest-get-type": "^27.5.1", - "jest-haste-map": "^27.5.1", - "jest-matcher-utils": "^27.5.1", - "jest-message-util": "^27.5.1", - "jest-util": "^27.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^27.5.1", - "semver": "^7.3.2" + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": ">=10" } }, + "node_modules/jest-snapshot/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/jest-util": { "version": "27.5.1", + "dev": true, "license": "MIT", "dependencies": { "@jest/types": "^27.5.1", @@ -18083,28 +20201,58 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/jest-util/node_modules/@jest/types": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-27.5.1.tgz", + "integrity": "sha512-Cx46iJ9QpwQTjIdq5VJu2QTMMs3QlEjI0x1QbBP5W1+nMzyc2XmimiRR/CbX9TO0cPTeUlxWMOu8mslYsJ8DEw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, "node_modules/jest-util/node_modules/ci-info": { "version": "3.3.0", + "dev": true, "license": "MIT" }, "node_modules/jest-validate": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dependencies": { - "@jest/types": "^27.5.1", + "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^27.5.1", + "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^27.5.1" + "pretty-format": "^29.7.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/jest-validate/node_modules/camelcase": { "version": "6.3.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "engines": { "node": ">=10" }, @@ -18112,37 +20260,128 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/jest-validate/node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, "node_modules/jest-watcher": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dependencies": { - "@jest/test-result": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "jest-util": "^27.5.1", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", "string-length": "^4.0.1" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker": { - "version": "27.5.1", - "license": "MIT", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dependencies": { "@types/node": "*", + "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", - "license": "MIT", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dependencies": { "has-flag": "^4.0.0" }, @@ -18222,60 +20461,6 @@ "version": "0.1.1", "license": "MIT" }, - "node_modules/jsdom": { - "version": "16.7.0", - "license": "MIT", - "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/acorn": { - "version": "8.7.1", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/jsesc": { "version": "2.5.2", "license": "MIT", @@ -18968,6 +21153,15 @@ "node": ">=8" } }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/loader-runner": { "version": "2.4.0", "license": "MIT", @@ -19050,6 +21244,11 @@ "version": "4.0.6", "license": "MIT" }, + "node_modules/lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha512-JPFqXFeZQ7BfS00H58kClY7SPVeHertPE0lNuCyZ26/XlN8TvakYD7b9bGyNmXbT/D3BbtPAAmq90gPWqLkxlQ==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "dev": true, @@ -19254,6 +21453,7 @@ "node_modules/make-dir": { "version": "3.1.0", "license": "MIT", + "peer": true, "dependencies": { "semver": "^6.0.0" }, @@ -19267,6 +21467,7 @@ "node_modules/make-dir/node_modules/semver": { "version": "6.3.0", "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" } @@ -20481,8 +22682,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.4", - "license": "MIT" + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/node-schedule": { "version": "2.1.1", @@ -21121,10 +23323,6 @@ "node": ">=0.10.0" } }, - "node_modules/nwsapi": { - "version": "2.2.0", - "license": "MIT" - }, "node_modules/oauth-sign": { "version": "0.9.0", "license": "Apache-2.0", @@ -21196,12 +23394,12 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -21252,6 +23450,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/objectorarray": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/objectorarray/-/objectorarray-1.0.5.tgz", + "integrity": "sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==" + }, "node_modules/omggif": { "version": "1.0.10", "license": "MIT" @@ -22272,10 +24475,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5": { - "version": "6.0.1", - "license": "MIT" - }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", @@ -22728,6 +24927,14 @@ "node": ">=0.10.0" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "dev": true, @@ -22745,17 +24952,22 @@ } }, "node_modules/prettier": { - "version": "2.2.1", - "license": "MIT", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "bin": { "prettier": "bin-prettier.js" }, "engines": { "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/pretty-format": { "version": "27.5.1", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1", @@ -22768,6 +24980,7 @@ }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -23000,10 +25213,25 @@ "bufferutil": { "optional": true }, - "utf-8-validate": { - "optional": true + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" } - } + ] }, "node_modules/q": { "version": "1.5.1", @@ -23103,6 +25331,7 @@ }, "node_modules/react-is": { "version": "17.0.2", + "dev": true, "license": "MIT" }, "node_modules/read-package-json": { @@ -23466,13 +25695,14 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" }, "engines": { "node": ">= 0.4" @@ -23698,6 +25928,19 @@ "node": ">= 6" } }, + "node_modules/release-please/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "license": "ISC", @@ -23843,8 +26086,9 @@ "license": "MIT" }, "node_modules/resolve.exports": { - "version": "1.1.0", - "license": "MIT", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", "engines": { "node": ">=10" } @@ -24004,12 +26248,12 @@ "license": "0BSD" }, "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -24037,14 +26281,17 @@ } }, "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -24060,22 +26307,16 @@ "version": "2.1.2", "license": "MIT" }, + "node_modules/sandbox": { + "resolved": "core/sandbox", + "link": true + }, "node_modules/sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", "peer": true }, - "node_modules/saxes": { - "version": "5.0.1", - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/scarlet": { "version": "2.0.20", "license": "MIT", @@ -25131,27 +27372,30 @@ "license": "ISC" }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dependencies": { - "define-data-property": "^1.0.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -25917,13 +28161,14 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -25933,26 +28178,29 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -26155,17 +28403,6 @@ "node": ">=8" } }, - "node_modules/supports-hyperlinks": { - "version": "2.2.0", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "license": "MIT", @@ -26176,10 +28413,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "license": "MIT" - }, "node_modules/tapable": { "version": "1.1.3", "license": "MIT", @@ -26322,18 +28555,41 @@ } } }, - "node_modules/terminal-link": { - "version": "2.1.1", - "license": "MIT", + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dev": true, "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" }, "engines": { - "node": ">=8" + "node": ">=6.0.0" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" } }, "node_modules/terser": { @@ -26500,10 +28756,6 @@ "real-require": "^0.2.0" } }, - "node_modules/throat": { - "version": "6.0.1", - "license": "MIT" - }, "node_modules/through": { "version": "2.3.8", "license": "MIT" @@ -26738,35 +28990,6 @@ "nopt": "bin/nopt.js" } }, - "node_modules/tough-cookie": { - "version": "4.0.0", - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.1.2", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/tr46": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/traverse": { "version": "0.6.7", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", @@ -26815,37 +29038,38 @@ "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" }, "node_modules/ts-jest": { - "version": "27.1.4", + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", + "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", "dev": true, - "license": "MIT", "dependencies": { "bs-logger": "0.x", "fast-json-stable-stringify": "2.x", - "jest-util": "^27.0.0", - "json5": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", "lodash.memoize": "4.x", "make-error": "1.x", - "semver": "7.x", - "yargs-parser": "20.x" + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" }, "bin": { "ts-jest": "cli.js" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { "@babel/core": ">=7.0.0-beta.0 <8", - "@types/jest": "^27.0.0", - "babel-jest": ">=27.0.0 <28", - "jest": "^27.0.0", - "typescript": ">=3.8 <5.0" + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" }, "peerDependenciesMeta": { "@babel/core": { "optional": true }, - "@types/jest": { + "@jest/types": { "optional": true }, "babel-jest": { @@ -26856,6 +29080,80 @@ } } }, + "node_modules/ts-jest/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-jest/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/ts-jest/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/ts-jest/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/ts-node": { "version": "8.10.2", "dev": true, @@ -26992,27 +29290,28 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -27022,15 +29321,16 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "has-proto": "^1.0.1", - "is-typed-array": "^1.1.10" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -27040,13 +29340,19 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", "dependencies": { - "call-bind": "^1.0.2", + "call-bind": "^1.0.7", "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -27059,21 +29365,22 @@ "node_modules/typedarray-to-buffer": { "version": "3.1.5", "license": "MIT", + "peer": true, "dependencies": { "is-typedarray": "^1.0.0" } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uglify-js": { @@ -27342,6 +29649,35 @@ "yarn": "*" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/update-notifier": { "version": "5.1.0", "license": "BSD-2-Clause", @@ -27450,24 +29786,18 @@ "license": "MIT" }, "node_modules/v8-to-istanbul": { - "version": "8.1.1", - "license": "ISC", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", + "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" + "convert-source-map": "^2.0.0" }, "engines": { "node": ">=10.12.0" } }, - "node_modules/v8-to-istanbul/node_modules/source-map": { - "version": "0.7.3", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "license": "Apache-2.0", @@ -27519,24 +29849,7 @@ }, "node_modules/vm-browserify": { "version": "1.1.2", - "license": "MIT" - }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, - "node_modules/w3c-xmlserializer": { - "version": "2.0.0", - "license": "MIT", - "dependencies": { - "xml-name-validator": "^3.0.0" - }, - "engines": { - "node": ">=10" - } + "license": "MIT" }, "node_modules/wait-port": { "version": "0.2.9", @@ -28002,13 +30315,6 @@ "node": ">= 8.11.0" } }, - "node_modules/webidl-conversions": { - "version": "6.1.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=10.4" - } - }, "node_modules/webpack": { "version": "4.46.0", "license": "MIT", @@ -28313,43 +30619,10 @@ "node": ">=0.10.0" } }, - "node_modules/whatwg-encoding": { - "version": "1.0.5", - "license": "MIT", - "dependencies": { - "iconv-lite": "0.4.24" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.4.24", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/whatwg-fetch": { "version": "3.6.2", "license": "MIT" }, - "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "8.7.0", - "license": "MIT", - "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/which": { "version": "1.3.1", "license": "ISC", @@ -28382,15 +30655,15 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -28409,7 +30682,6 @@ "node_modules/widest-line": { "version": "3.1.0", "license": "MIT", - "peer": true, "dependencies": { "string-width": "^4.0.0" }, @@ -28422,11 +30694,11 @@ "license": "MIT" }, "node_modules/winston": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.8.2.tgz", - "integrity": "sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz", + "integrity": "sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==", "dependencies": { - "@colors/colors": "1.5.0", + "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", @@ -28436,26 +30708,36 @@ "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.5.0" + "winston-transport": "^4.7.0" }, "engines": { "node": ">= 12.0.0" } }, "node_modules/winston-transport": { - "version": "4.5.0", - "license": "MIT", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", + "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", "dependencies": { "logform": "^2.3.2", "readable-stream": "^3.6.0", "triple-beam": "^1.3.0" }, "engines": { - "node": ">= 6.4.0" + "node": ">= 12.0.0" + } + }, + "node_modules/winston/node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" } }, "node_modules/word-wrap": { "version": "1.2.3", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -28499,6 +30781,7 @@ "node_modules/write-file-atomic": { "version": "3.0.3", "license": "ISC", + "peer": true, "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", @@ -28600,10 +30883,6 @@ "node": ">=8" } }, - "node_modules/xml-name-validator": { - "version": "3.0.0", - "license": "Apache-2.0" - }, "node_modules/xml2js": { "version": "0.4.19", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", @@ -28623,10 +30902,6 @@ "node": ">=4.0" } }, - "node_modules/xmlchars": { - "version": "2.2.0", - "license": "MIT" - }, "node_modules/xpath": { "version": "0.0.32", "dev": true, @@ -28656,7 +30931,8 @@ }, "node_modules/yaml": { "version": "1.10.2", - "license": "ISC", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "engines": { "node": ">= 6" } @@ -28692,7 +30968,6 @@ }, "node_modules/yargs": { "version": "17.4.1", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^7.0.2", @@ -28760,7 +31035,6 @@ }, "node_modules/yargs/node_modules/yargs-parser": { "version": "21.0.1", - "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -28805,47 +31079,73 @@ } }, "node_modules/zod": { - "version": "3.20.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.6.tgz", - "integrity": "sha512-oyu0m54SGCtzh6EClBVqDDlAYRz4jrVtKwQ7ZnsEmMI9HnzuZFj8QFwAY1M5uniIYACdGvv0PBWPF2kO0aNofA==", + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", "funding": { "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/zod-validation-error": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-0.3.1.tgz", - "integrity": "sha512-3m7VcE4YT3ZdGJoYBZnLt8E1S/r8zPFNDWMc2njaeC7Vxe5FQbgIiwHoerSkli5PgsUyxWQhpFxEAJXkBJvoDg==", - "engines": { - "node": "^14.17 || >=16.0.0" + "node_modules/zod2md": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/zod2md/-/zod2md-0.1.2.tgz", + "integrity": "sha512-+pWpY+4yLKCnNyLeVi7RQLfPkKuMqbSwiFPy83bbvM+ZYi+B54lCcBPDBcRecrSH4VMxD7gkIeH8mKr4/vF9Bg==", + "dev": true, + "dependencies": { + "@commander-js/extra-typings": "^12.0.0", + "bundle-require": "^4.0.2", + "commander": "^12.0.0", + "esbuild": "^0.19.11" + }, + "bin": { + "zod2md": "dist/bin.js" }, "peerDependencies": { - "zod": "^3.18.0" + "zod": "^3.22.0" + } + }, + "node_modules/zod2md/node_modules/@commander-js/extra-typings": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-12.0.1.tgz", + "integrity": "sha512-OvkMobb1eMqOCuJdbuSin/KJkkZr7n24/UNV+Lcz/0Dhepf3r2p9PaGwpRpAWej7A+gQnny4h8mGhpFl4giKkg==", + "dev": true, + "peerDependencies": { + "commander": "~12.0.0" + } + }, + "node_modules/zod2md/node_modules/commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "dev": true, + "engines": { + "node": ">=18" } }, "plugins/babel": { "name": "@dotcom-tool-kit/babel", - "version": "3.2.0", + "version": "4.0.0-beta.5", "license": "MIT", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "fast-glob": "^3.2.11", "tslib": "^2.3.1" }, "devDependencies": { "@babel/preset-env": "^7.16.11", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { "@babel/core": "7.x", - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/babel/node_modules/tslib": { @@ -28855,64 +31155,66 @@ }, "plugins/backend-app": { "name": "@dotcom-tool-kit/backend-app", - "version": "3.2.0", + "version": "4.0.0-beta.6", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/backend-heroku-app": "^3.1.0" + "@dotcom-tool-kit/backend-heroku-app": "4.0.0-beta.6" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/backend-heroku-app": { "name": "@dotcom-tool-kit/backend-heroku-app", - "version": "3.1.0", + "version": "4.0.0-beta.6", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/circleci-deploy": "^3.3.0", - "@dotcom-tool-kit/heroku": "^3.4.0", - "@dotcom-tool-kit/node": "^3.4.0", - "@dotcom-tool-kit/npm": "^3.3.0" + "@dotcom-tool-kit/circleci-deploy": "4.0.0-beta.6", + "@dotcom-tool-kit/heroku": "4.0.0-beta.5", + "@dotcom-tool-kit/node": "4.0.0-beta.5", + "@dotcom-tool-kit/npm": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/backend-serverless-app": { "name": "@dotcom-tool-kit/backend-serverless-app", - "version": "3.1.0", + "version": "4.0.0-beta.6", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/circleci-deploy": "^3.3.0", - "@dotcom-tool-kit/node": "^3.4.0", - "@dotcom-tool-kit/npm": "^3.3.0", - "@dotcom-tool-kit/serverless": "^2.3.0" + "@dotcom-tool-kit/circleci-deploy": "4.0.0-beta.6", + "@dotcom-tool-kit/node": "4.0.0-beta.5", + "@dotcom-tool-kit/npm": "4.0.0-beta.5", + "@dotcom-tool-kit/serverless": "3.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/circleci": { "name": "@dotcom-tool-kit/circleci", - "version": "5.4.0", + "version": "7.0.0-beta.6", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/state": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/conflict": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "jest-diff": "^29.5.0", "lodash": "^4.17.21", "tslib": "^2.3.1", @@ -28920,37 +31222,41 @@ "yaml": "^2.1.1" }, "devDependencies": { + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "@types/jest": "^27.4.0", "@types/js-yaml": "^4.0.3", "@types/lodash": "^4.14.185", - "winston": "^3.5.1" + "winston": "^3.5.1", + "zod": "^3.22.4" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5", + "zod": "^3.22.4" } }, "plugins/circleci-deploy": { "name": "@dotcom-tool-kit/circleci-deploy", - "version": "3.3.0", + "version": "4.0.0-beta.6", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/circleci": "^5.4.0", + "@dotcom-tool-kit/circleci": "7.0.0-beta.6", "tslib": "^2.3.1" }, "devDependencies": { "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/circleci-deploy/node_modules/tslib": { @@ -28960,19 +31266,19 @@ }, "plugins/circleci-heroku": { "name": "@dotcom-tool-kit/circleci-heroku", - "version": "3.2.0", + "version": "4.0.0-beta.6", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/circleci-deploy": "^3.3.0", - "@dotcom-tool-kit/heroku": "^3.4.0", + "@dotcom-tool-kit/circleci-deploy": "4.0.0-beta.6", + "@dotcom-tool-kit/heroku": "4.0.0-beta.5", "tslib": "^2.3.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/circleci-heroku/node_modules/tslib": { @@ -28982,20 +31288,19 @@ }, "plugins/circleci-npm": { "name": "@dotcom-tool-kit/circleci-npm", - "version": "5.3.0", + "version": "6.0.0-beta.6", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/circleci": "^5.4.0", - "@dotcom-tool-kit/npm": "^3.3.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/circleci": "7.0.0-beta.6", + "@dotcom-tool-kit/npm": "4.0.0-beta.5", "tslib": "^2.3.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/circleci-npm/node_modules/tslib": { @@ -29104,60 +31409,67 @@ }, "plugins/component": { "name": "@dotcom-tool-kit/component", - "version": "4.1.0", + "version": "5.0.0-beta.6", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/circleci-npm": "^5.3.0", - "@dotcom-tool-kit/npm": "^3.3.0" + "@dotcom-tool-kit/circleci-npm": "6.0.0-beta.6", + "@dotcom-tool-kit/npm": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/cypress": { "name": "@dotcom-tool-kit/cypress", - "version": "3.4.0", + "version": "5.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/doppler": "^1.1.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/state": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0" + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", + "@dotcom-tool-kit/state": "4.0.0-beta.0" + }, + "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/eslint": { "name": "@dotcom-tool-kit/eslint", - "version": "3.2.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "tslib": "^2.3.1" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "@types/eslint": "^7.2.13", + "@types/temp": "^0.9.4", "eslint": "^8.15.0", + "temp": "^0.9.4", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x", + "dotcom-tool-kit": "4.0.0-beta.5", "eslint": "7.x || 8.x" } }, @@ -29360,35 +31672,35 @@ }, "plugins/frontend-app": { "name": "@dotcom-tool-kit/frontend-app", - "version": "3.2.0", + "version": "4.0.0-beta.6", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/backend-heroku-app": "^3.1.0", - "@dotcom-tool-kit/upload-assets-to-s3": "^3.2.0", - "@dotcom-tool-kit/webpack": "^3.2.0" + "@dotcom-tool-kit/backend-heroku-app": "4.0.0-beta.6", + "@dotcom-tool-kit/upload-assets-to-s3": "4.0.0-beta.5", + "@dotcom-tool-kit/webpack": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/heroku": { "name": "@dotcom-tool-kit/heroku", - "version": "3.4.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/doppler": "^1.1.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/npm": "^3.3.0", - "@dotcom-tool-kit/options": "^3.2.0", - "@dotcom-tool-kit/package-json-hook": "^4.2.0", - "@dotcom-tool-kit/state": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", - "@dotcom-tool-kit/wait-for-ok": "^3.2.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/npm": "4.0.0-beta.5", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", + "@dotcom-tool-kit/state": "4.0.0-beta.0", + "@dotcom-tool-kit/wait-for-ok": "4.0.0-beta.0", "@octokit/request": "^5.6.0", "@octokit/request-error": "^2.1.0", "heroku-client": "^3.1.0", @@ -29397,16 +31709,17 @@ "tslib": "^2.3.1" }, "devDependencies": { - "@types/financial-times__package-json": "^1.9.0", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", + "@types/financial-times__package-json": "2.0.0-beta.0", "@types/p-retry": "^3.0.1", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/heroku/node_modules/tslib": { @@ -29416,18 +31729,18 @@ }, "plugins/husky-npm": { "name": "@dotcom-tool-kit/husky-npm", - "version": "4.2.0", + "version": "5.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/package-json-hook": "^4.2.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", "tslib": "^2.3.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x", + "dotcom-tool-kit": "4.0.0-beta.5", "husky": "4.x" } }, @@ -29438,23 +31751,23 @@ }, "plugins/jest": { "name": "@dotcom-tool-kit/jest", - "version": "3.4.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "tslib": "^2.3.1" }, "devDependencies": { - "@jest/globals": "^27.4.6", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x", + "dotcom-tool-kit": "4.0.0-beta.5", "jest-cli": "27.x || 28.x || 29.x" } }, @@ -29465,39 +31778,40 @@ }, "plugins/lint-staged": { "name": "@dotcom-tool-kit/lint-staged", - "version": "4.2.0", + "version": "5.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/package-json-hook": "^4.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", "lint-staged": "^11.2.3", "tslib": "^2.3.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/lint-staged-npm": { "name": "@dotcom-tool-kit/lint-staged-npm", - "version": "3.2.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/husky-npm": "^4.2.0", - "@dotcom-tool-kit/lint-staged": "^4.2.0", - "@dotcom-tool-kit/options": "^3.2.0", + "@dotcom-tool-kit/husky-npm": "5.0.0-beta.5", + "@dotcom-tool-kit/lint-staged": "5.0.0-beta.5", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", "tslib": "^2.3.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/lint-staged-npm/node_modules/tslib": { @@ -29562,27 +31876,28 @@ }, "plugins/mocha": { "name": "@dotcom-tool-kit/mocha", - "version": "3.2.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "glob": "^7.1.7", "tslib": "^2.3.1" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "@types/glob": "^7.1.3", "@types/mocha": "^8.2.2", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x", + "dotcom-tool-kit": "4.0.0-beta.5", "mocha": ">=6.x <=10.x" } }, @@ -29591,28 +31906,32 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, + "plugins/monorepo": { + "extraneous": true + }, "plugins/n-test": { "name": "@dotcom-tool-kit/n-test", - "version": "3.3.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/state": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "@financial-times/n-test": "^6.1.0-beta.1", "tslib": "^2.3.1" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "@types/jest": "^27.4.0", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/n-test/node_modules/@financial-times/n-test": { @@ -29714,23 +32033,26 @@ }, "plugins/next-router": { "name": "@dotcom-tool-kit/next-router", - "version": "3.4.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/doppler": "^1.1.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/state": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "ft-next-router": "^3.0.0", "tslib": "^2.3.1" }, + "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0" + }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/next-router/node_modules/@financial-times/n-flags-client": { @@ -29845,23 +32167,26 @@ }, "plugins/node": { "name": "@dotcom-tool-kit/node", - "version": "3.4.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/doppler": "^1.1.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/state": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "get-port": "^5.1.1", "tslib": "^2.3.1", "wait-port": "^0.2.9" }, + "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0" + }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/node/node_modules/tslib": { @@ -29871,25 +32196,26 @@ }, "plugins/nodemon": { "name": "@dotcom-tool-kit/nodemon", - "version": "3.4.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/doppler": "^1.1.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/state": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "get-port": "^5.1.1", "tslib": "^2.3.1" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@types/nodemon": "^1.19.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x", + "dotcom-tool-kit": "4.0.0-beta.5", "nodemon": "2.x" } }, @@ -29900,14 +32226,14 @@ }, "plugins/npm": { "name": "@dotcom-tool-kit/npm", - "version": "3.3.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { "@actions/exec": "^1.1.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/package-json-hook": "^4.2.0", - "@dotcom-tool-kit/state": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "libnpmpack": "^3.1.0", "libnpmpublish": "^5.0.1", "pacote": "^12.0.3", @@ -29915,17 +32241,18 @@ "tslib": "^2.3.1" }, "devDependencies": { - "@types/libnpmpublish": "^4.0.1", + "@npm/types": "^1.0.2", + "@types/libnpmpublish": "^4.0.6", "@types/pacote": "^11.1.3", - "@types/tar": "^6.1.1", + "@types/tar": "^6.1.10", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/npm/node_modules/@npmcli/git": { @@ -29943,6 +32270,36 @@ "which": "^2.0.2" } }, + "plugins/npm/node_modules/@types/libnpmpublish": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/libnpmpublish/-/libnpmpublish-4.0.6.tgz", + "integrity": "sha512-xI99EEgpr1R0hpLAKb52QbBYv8ZZa9FDiZS7LEVE6RevjkQHF3BflPR7Mo2F8yxMnqP6eoPkWE0bnWvDC/sA9A==", + "dev": true, + "dependencies": { + "@npm/types": "*", + "@types/node-fetch": "*", + "@types/npm-registry-fetch": "*" + } + }, + "plugins/npm/node_modules/@types/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-ThA1WD8aDdVU4VLuyq5NEqriwXErF5gEIJeyT6gHBWU7JtSmW2a5qjNv3/vR82O20mW+1vhmeZJfBQPT3HCugg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "minipass": "^4.0.0" + } + }, + "plugins/npm/node_modules/@types/tar/node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "plugins/npm/node_modules/ignore-walk": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-4.0.1.tgz", @@ -30086,10 +32443,10 @@ }, "plugins/pa11y": { "name": "@dotcom-tool-kit/pa11y", - "version": "0.5.2", + "version": "1.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", "pa11y-ci": "^3.0.1", "tslib": "^2.3.1" }, @@ -30097,11 +32454,11 @@ "@types/pa11y": "^5.3.4" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/pa11y/node_modules/tslib": { @@ -30109,34 +32466,87 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, + "plugins/package-json-hook": { + "name": "@dotcom-tool-kit/package-json-hook", + "version": "5.0.0-beta.2", + "license": "ISC", + "dependencies": { + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/conflict": "2.0.0-beta.0", + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", + "@financial-times/package-json": "^3.0.0", + "lodash": "^4.17.21", + "tslib": "^2.3.1" + }, + "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", + "@jest/globals": "^27.4.6", + "@types/lodash": "^4.14.185", + "winston": "^3.5.1", + "zod": "^3.22.4" + }, + "engines": { + "node": "18.x || 20.x", + "npm": "7.x || 8.x || 9.x || 10.x" + }, + "peerDependencies": { + "zod": "^3.22.4" + } + }, + "plugins/package-json-hook/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "plugins/parallel": { + "name": "@dotcom-tool-kit/parallel", + "version": "0.1.0", + "license": "ISC", + "dependencies": { + "@dotcom-tool-kit/base": "^4.0.0-beta.0", + "@dotcom-tool-kit/schemas": "^2.0.0-beta.0", + "@dotcom-tool-kit/task": "^1.2.0" + }, + "engines": { + "node": "18.x || 20.x", + "npm": "7.x || 8.x || 9.x" + }, + "peerDependencies": { + "dotcom-tool-kit": "4.0.0-beta.5" + } + }, "plugins/prettier": { "name": "@dotcom-tool-kit/prettier", - "version": "3.2.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/package-json-hook": "^4.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", "fast-glob": "^3.2.7", "hook-std": "^2.0.0", "prettier": "^2.2.1", "tslib": "^2.3.1" }, "devDependencies": { - "@jest/globals": "^27.4.6", - "jest": "^27.4.7", - "ts-jest": "^27.1.3", + "@types/prettier": "^2.7.3", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, + "plugins/prettier/node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", + "dev": true + }, "plugins/prettier/node_modules/tslib": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", @@ -30163,24 +32573,27 @@ }, "plugins/serverless": { "name": "@dotcom-tool-kit/serverless", - "version": "2.3.0", + "version": "3.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/doppler": "^1.1.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/options": "^3.2.0", - "@dotcom-tool-kit/state": "^3.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "get-port": "^5.1.1", "tslib": "^2.3.1", "wait-port": "^0.2.9" }, + "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0" + }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x", + "dotcom-tool-kit": "4.0.0-beta.5", "serverless-offline": "^12.0.4" } }, @@ -30191,24 +32604,25 @@ }, "plugins/typescript": { "name": "@dotcom-tool-kit/typescript", - "version": "2.2.0", + "version": "3.0.0-beta.5", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0" + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^29.3.1", "typescript": "^4.9.4", "winston": "^3.8.2" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x", - "typescript": "3.x || 4.x" + "dotcom-tool-kit": "4.0.0-beta.5", + "typescript": "3.x || 4.x || 5.x" } }, "plugins/typescript/node_modules/@babel/code-frame": { @@ -30397,21 +32811,35 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "plugins/typescript/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "plugins/upload-assets-to-s3": { "name": "@dotcom-tool-kit/upload-assets-to-s3", - "version": "3.2.0", + "version": "4.0.0-beta.5", "license": "ISC", "dependencies": { "@aws-sdk/client-s3": "^3.256.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "glob": "^7.1.6", "mime": "^2.5.2", "tslib": "^2.3.1" }, "devDependencies": { "@aws-sdk/types": "^3.13.1", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "@types/glob": "^7.1.3", "@types/jest": "^27.4.0", @@ -30419,11 +32847,11 @@ "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" } }, "plugins/upload-assets-to-s3/node_modules/tslib": { @@ -30433,27 +32861,28 @@ }, "plugins/webpack": { "name": "@dotcom-tool-kit/webpack", - "version": "3.2.0", + "version": "4.0.0-beta.5", "license": "MIT", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "tslib": "^2.3.1", "webpack-cli": "^4.6.0" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "ts-node": "^10.0.0", "webpack": "^4.42.1", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "peerDependencies": { - "dotcom-tool-kit": "3.x", + "dotcom-tool-kit": "4.0.0-beta.5", "webpack": "4.x.x || 5.x.x" } }, @@ -30533,7 +32962,7 @@ }, "types/financial-times__package-json": { "name": "@types/financial-times__package-json", - "version": "1.9.0", + "version": "2.0.0-beta.0", "license": "ISC" } } diff --git a/package.json b/package.json index daa70c68c..54bd3bf64 100644 --- a/package.json +++ b/package.json @@ -30,16 +30,19 @@ "@typescript-eslint/eslint-plugin": "^5.57.0", "@typescript-eslint/parser": "^5.57.0", "check-engines": "^1.5.0", + "endent": "^2.1.0", "eslint": "^8.37.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.5", "husky": "^4.3.8", - "jest": "^27.4.7", + "jest": "^29.7.0", "lint-staged": "^10.5.4", - "prettier": "2.2.1", + "prettier": "^2.8.8", "release-please": "^15.0.0", - "ts-jest": "^27.1.3", - "typescript": "~4.9.5" + "ts-jest": "^29.1.2", + "typescript": "~5.4.5", + "winston": "^3.13.0", + "zod2md": "^0.1.2" }, "workspaces": [ "core/*", @@ -50,6 +53,7 @@ "husky": { "hooks": { "pre-commit": "lint-staged", + "pre-push": "./scripts/generate-and-commit-docs.sh", "commit-msg": "commitlint --edit", "post-checkout": "npm run clean-up-packages -- $HUSKY_GIT_PARAMS" } @@ -58,11 +62,11 @@ "@types/superagent": "^4.1.10" }, "volta": { - "node": "20.10.0" + "node": "20.12.2" }, "engines": { - "node": "16.x || 18.x || 20.x", - "npm": "7.x || 8.x || 9.x || 10.x" + "node": "18.x || 20.x", + "npm": "8.x || 9.x || 10.x" }, "overrides": { "type-fest": "3.6.0" diff --git a/plugins/babel/.toolkitrc.yml b/plugins/babel/.toolkitrc.yml index a583f6fdd..ba5da3398 100644 --- a/plugins/babel/.toolkitrc.yml +++ b/plugins/babel/.toolkitrc.yml @@ -1,4 +1,9 @@ -hooks: - 'build:local': BabelDevelopment - 'build:ci': BabelProduction - 'build:remote': BabelProduction +tasks: + Babel: './lib/tasks/babel' + +commands: + 'build:local': Babel + 'build:ci': Babel + 'build:remote': Babel + +version: 2 diff --git a/plugins/babel/jest.config.js b/plugins/babel/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/babel/jest.config.js +++ b/plugins/babel/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/babel/package.json b/plugins/babel/package.json index 6e06aeacb..6e4d23448 100644 --- a/plugins/babel/package.json +++ b/plugins/babel/package.json @@ -1,7 +1,6 @@ { "name": "@dotcom-tool-kit/babel", - "version": "3.2.0", - "main": "lib", + "version": "4.0.0-beta.5", "description": "", "author": "FT.com Platforms Team ", "license": "MIT", @@ -17,9 +16,9 @@ }, "keywords": [], "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "fast-glob": "^3.2.11", "tslib": "^2.3.1" }, @@ -32,15 +31,16 @@ }, "devDependencies": { "@babel/preset-env": "^7.16.11", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "winston": "^3.5.1" }, "peerDependencies": { "@babel/core": "7.x", - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/babel/readme.md b/plugins/babel/readme.md new file mode 100644 index 000000000..159183fd3 --- /dev/null +++ b/plugins/babel/readme.md @@ -0,0 +1,36 @@ +# @dotcom-tool-kit/babel + +A plugin to run [Babel](https://babeljs.io/) to compile ES2015+ syntax. + +## Installation + +With Tool Kit [already set up](https://github.com/financial-times/dotcom-tool-kit#installing-and-using-tool-kit), install this plugin as a dev dependency: + +```sh +npm install --save-dev @dotcom-tool-kit/jest +``` + +And add it to your repo's `.toolkitrc.yml`: + +```yaml +plugins: + - '@dotcom-tool-kit/jest' +``` + + +## Tasks + +### `Babel` + +Compile files with Babel +#### Task options + +| Property | Description | Type | Default | +| :----------------- | :---------------------------------------------------------------------------- | :------------------------------ | :-------------- | +| `files` | a glob pattern of files to build in your repo | `string` | `'src/**/*.js'` | +| `outputPath` | folder to output built files into | `string` | `'lib'` | +| `configFile` | path to the Babel [config file](https://babeljs.io/docs/configuration) to use | `string` | | +| **`envName`** (\*) | the Babel [environment](https://babeljs.io/docs/options#env) to use | `'production' \| 'development'` | | + +_(\*) Required._ + diff --git a/plugins/babel/src/index.ts b/plugins/babel/src/index.ts deleted file mode 100644 index 20e63ace5..000000000 --- a/plugins/babel/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import BabelDevelopment from './tasks/development' -import BabelProduction from './tasks/production' - -export const tasks = [BabelDevelopment, BabelProduction] diff --git a/plugins/babel/src/run-babel.ts b/plugins/babel/src/run-babel.ts deleted file mode 100644 index 1f3f74d48..000000000 --- a/plugins/babel/src/run-babel.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ToolKitError } from '@dotcom-tool-kit/error' -import { hookConsole } from '@dotcom-tool-kit/logger' -import type { BabelOptions } from '@dotcom-tool-kit/types/src/schema/babel' -import * as babel from '@babel/core' -import fg from 'fast-glob' -import { promises as fs } from 'fs' -import path from 'path' -import type { Logger } from 'winston' - -export async function runBabel( - logger: Logger, - options: BabelOptions, - transformOptions?: babel.TransformOptions -): Promise { - const fileGlob = options.files ?? 'src/**/*.js' - const files = await fg(fileGlob) - // Work out the root of the glob so we can strip this part of the path out of - // the outputted files. - // E.g., a glob of 'src/**/*.js' = src/a/b.js -> lib/a/b.js - // a glob of 'src/a/**/*.js' = src/a/b.js -> lib/b.js - const { base } = fg.generateTasks(fileGlob)[0] - - const outputPath = options.outputPath ?? 'lib' - - if (options.configFile) { - const { configFile } = options - transformOptions = transformOptions ? { ...transformOptions, configFile } : { configFile } - } - - logger.info('running babel') - const unhook = hookConsole(logger, 'babel') - await Promise.all( - files.map(async (file) => { - const transformed = await babel.transformFileAsync(file, transformOptions) - if (!transformed?.code) { - const error = new ToolKitError('Babel failed to generate code') - error.details = `the problematic file was ${file}` - throw error - } - // Create parent directories if they don't exist before creating child file of transpiled code. - const filePath = path.join(outputPath, path.relative(base, file)) - await fs.mkdir(path.dirname(filePath), { recursive: true }) - await fs.writeFile(filePath, transformed.code) - }) - ) - unhook() -} diff --git a/plugins/babel/src/tasks/babel.ts b/plugins/babel/src/tasks/babel.ts new file mode 100644 index 000000000..5c7427c1c --- /dev/null +++ b/plugins/babel/src/tasks/babel.ts @@ -0,0 +1,48 @@ +import { promises as fs } from 'fs' +import path from 'path' + +import * as babel from '@babel/core' +import fg from 'fast-glob' + +import { type BabelSchema } from '@dotcom-tool-kit/schemas/lib/tasks/babel' +import { Task } from '@dotcom-tool-kit/base' +import { ToolKitError } from '@dotcom-tool-kit/error' +import { hookConsole } from '@dotcom-tool-kit/logger' + +export default class Babel extends Task<{ task: typeof BabelSchema }> { + async run(): Promise { + const fileGlob = this.options.files + const files = await fg(fileGlob) + // Work out the root of the glob so we can strip this part of the path out of + // the outputted files. + // E.g., a glob of 'src/**/*.js' = src/a/b.js -> lib/a/b.js + // a glob of 'src/a/**/*.js' = src/a/b.js -> lib/b.js + const { base } = fg.generateTasks(fileGlob)[0] + + const outputPath = this.options.outputPath + + this.logger.info('running babel') + const unhook = hookConsole(this.logger, 'babel') + + await Promise.all( + files.map(async (file) => { + const transformed = await babel.transformFileAsync(file, { + configFile: this.options.configFile, + envName: this.options.envName + }) + // TODO better error handling + if (!transformed?.code) { + const error = new ToolKitError('Babel failed to generate code') + error.details = `the problematic file was ${file}` + throw error + } + // Create parent directories if they don't exist before creating child file of transpiled code. + const filePath = path.join(outputPath, path.relative(base, file)) + await fs.mkdir(path.dirname(filePath), { recursive: true }) + await fs.writeFile(filePath, transformed.code) + }) + ) + + unhook() + } +} diff --git a/plugins/babel/src/tasks/development.ts b/plugins/babel/src/tasks/development.ts deleted file mode 100644 index 97186f885..000000000 --- a/plugins/babel/src/tasks/development.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { runBabel } from '../run-babel' -import { Task } from '@dotcom-tool-kit/types' -import { BabelSchema } from '@dotcom-tool-kit/types/lib/schema/babel' - -export default class BabelDevelopment extends Task { - static description = 'build babel' - - async run(): Promise { - await runBabel(this.logger, this.options) - } -} diff --git a/plugins/babel/src/tasks/production.ts b/plugins/babel/src/tasks/production.ts deleted file mode 100644 index 03fb0e4f3..000000000 --- a/plugins/babel/src/tasks/production.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { runBabel } from '../run-babel' -import { Task } from '@dotcom-tool-kit/types' -import { BabelSchema } from '@dotcom-tool-kit/types/lib/schema/babel' - -export default class BabelProduction extends Task { - static description = 'build babel' - - async run(): Promise { - await runBabel(this.logger, this.options, { envName: 'production' }) - } -} diff --git a/plugins/babel/test/files/fixtures/transpiled.js b/plugins/babel/test/files/fixtures/transpiled.js deleted file mode 100644 index ed1234556..000000000 --- a/plugins/babel/test/files/fixtures/transpiled.js +++ /dev/null @@ -1,10 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.test = test; - -function test(x) { - return x !== null && x !== void 0 ? x : 0; -} \ No newline at end of file diff --git a/plugins/babel/test/tasks/babel.test.ts b/plugins/babel/test/tasks/babel.test.ts index 94ab2ca78..c3dfcdc35 100644 --- a/plugins/babel/test/tasks/babel.test.ts +++ b/plugins/babel/test/tasks/babel.test.ts @@ -1,29 +1,39 @@ -import { beforeAll, describe, expect, it } from '@jest/globals' -import Babel from '../../src/tasks/development' +import { describe, expect, it } from '@jest/globals' +import Babel from '../../src/tasks/babel' import { promises as fs } from 'fs' import * as path from 'path' import winston, { Logger } from 'winston' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger const testDirectory = path.join(__dirname, '../files') const outputPath = path.join(testDirectory, 'lib') describe('babel', () => { - let transpiledFixture: string - - beforeAll(async () => { - transpiledFixture = await fs.readFile(path.join(testDirectory, 'fixtures/transpiled.js'), 'utf8') - }) - it('should transpile the file', async () => { - const task = new Babel(logger, { - files: path.join(testDirectory, 'index.js'), - outputPath, - configFile: path.join(testDirectory, 'babel.config.json') - }) + const task = new Babel( + logger, + 'Babel', + {}, + { + envName: 'development', + files: path.join(testDirectory, 'index.js'), + outputPath, + configFile: path.join(testDirectory, 'babel.config.json') + } + ) await task.run() const transpiled = await fs.readFile(path.join(outputPath, 'index.js'), 'utf8') - expect(transpiled).toEqual(transpiledFixture) + expect(transpiled).toMatchInlineSnapshot(` + ""use strict"; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.test = test; + function test(x) { + return x !== null && x !== void 0 ? x : 0; + }" + `) }) }) diff --git a/plugins/babel/tsconfig.json b/plugins/babel/tsconfig.json index 3f2a5796a..459bea939 100644 --- a/plugins/babel/tsconfig.json +++ b/plugins/babel/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.settings.json", - "include": ["src/**/*"], + "include": [ + "src/**/*" + ], "compilerOptions": { "outDir": "lib", "rootDir": "src" @@ -13,7 +15,10 @@ "path": "../../lib/logger" }, { - "path": "../../lib/types" + "path": "../../lib/base" + }, + { + "path": "../../lib/schemas" } ] } diff --git a/plugins/backend-app/.toolkitrc.yml b/plugins/backend-app/.toolkitrc.yml index 08ec2cb27..17d4d01f7 100644 --- a/plugins/backend-app/.toolkitrc.yml +++ b/plugins/backend-app/.toolkitrc.yml @@ -1,2 +1,4 @@ plugins: - '@dotcom-tool-kit/backend-heroku-app' + +version: 2 diff --git a/plugins/backend-app/index.js b/plugins/backend-app/index.js index 2793ef369..e69de29bb 100644 --- a/plugins/backend-app/index.js +++ b/plugins/backend-app/index.js @@ -1 +0,0 @@ -exports.tasks = [] \ No newline at end of file diff --git a/plugins/backend-app/package.json b/plugins/backend-app/package.json index 34ba99c77..bafdb1628 100644 --- a/plugins/backend-app/package.json +++ b/plugins/backend-app/package.json @@ -1,13 +1,13 @@ { "name": "@dotcom-tool-kit/backend-app", - "version": "3.2.4", + "version": "4.0.0-beta.6", "description": "", "main": "index.js", "keywords": [], "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/backend-heroku-app": "^3.1.4" + "@dotcom-tool-kit/backend-heroku-app": "4.0.0-beta.6" }, "repository": { "type": "git", @@ -17,10 +17,10 @@ "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/backend-app", "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/backend-heroku-app/.toolkitrc.yml b/plugins/backend-heroku-app/.toolkitrc.yml index aff1e4846..ec0dd8ed0 100644 --- a/plugins/backend-heroku-app/.toolkitrc.yml +++ b/plugins/backend-heroku-app/.toolkitrc.yml @@ -4,9 +4,11 @@ plugins: - '@dotcom-tool-kit/heroku' - '@dotcom-tool-kit/node' -hooks: +commands: 'run:local': Node 'deploy:review': HerokuReview 'deploy:staging': HerokuStaging 'deploy:production': HerokuProduction 'teardown:staging': HerokuTeardown + +version: 2 diff --git a/plugins/backend-heroku-app/index.js b/plugins/backend-heroku-app/index.js index c32ed7534..e69de29bb 100644 --- a/plugins/backend-heroku-app/index.js +++ b/plugins/backend-heroku-app/index.js @@ -1 +0,0 @@ -exports.tasks = [] diff --git a/plugins/backend-heroku-app/package.json b/plugins/backend-heroku-app/package.json index cb90849a0..5de2fb69e 100644 --- a/plugins/backend-heroku-app/package.json +++ b/plugins/backend-heroku-app/package.json @@ -1,16 +1,16 @@ { "name": "@dotcom-tool-kit/backend-heroku-app", - "version": "3.1.4", + "version": "4.0.0-beta.6", "description": "", "main": "index.js", "keywords": [], "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/circleci-deploy": "^3.4.3", - "@dotcom-tool-kit/heroku": "^3.4.1", - "@dotcom-tool-kit/node": "^3.4.1", - "@dotcom-tool-kit/npm": "^3.3.1" + "@dotcom-tool-kit/circleci-deploy": "4.0.0-beta.6", + "@dotcom-tool-kit/heroku": "4.0.0-beta.5", + "@dotcom-tool-kit/node": "4.0.0-beta.5", + "@dotcom-tool-kit/npm": "4.0.0-beta.5" }, "repository": { "type": "git", @@ -20,10 +20,10 @@ "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/backend-heroku-app", "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/backend-serverless-app/.toolkitrc.yml b/plugins/backend-serverless-app/.toolkitrc.yml index 1e100bc76..3ac88b102 100644 --- a/plugins/backend-serverless-app/.toolkitrc.yml +++ b/plugins/backend-serverless-app/.toolkitrc.yml @@ -4,8 +4,10 @@ plugins: - '@dotcom-tool-kit/serverless' - '@dotcom-tool-kit/node' -hooks: +commands: 'run:local': ServerlessRun 'deploy:review': ServerlessProvision 'deploy:production': ServerlessDeploy 'teardown:review': ServerlessTeardown + +version: 2 diff --git a/plugins/backend-serverless-app/index.js b/plugins/backend-serverless-app/index.js index c32ed7534..e69de29bb 100644 --- a/plugins/backend-serverless-app/index.js +++ b/plugins/backend-serverless-app/index.js @@ -1 +0,0 @@ -exports.tasks = [] diff --git a/plugins/backend-serverless-app/package.json b/plugins/backend-serverless-app/package.json index b945f3e34..1dbf53244 100644 --- a/plugins/backend-serverless-app/package.json +++ b/plugins/backend-serverless-app/package.json @@ -1,16 +1,16 @@ { "name": "@dotcom-tool-kit/backend-serverless-app", - "version": "3.2.7", + "version": "4.0.0-beta.6", "description": "", "main": "index.js", "keywords": [], "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/circleci-deploy": "^3.4.3", - "@dotcom-tool-kit/node": "^3.4.1", - "@dotcom-tool-kit/npm": "^3.3.1", - "@dotcom-tool-kit/serverless": "^2.4.4" + "@dotcom-tool-kit/circleci-deploy": "4.0.0-beta.6", + "@dotcom-tool-kit/node": "4.0.0-beta.5", + "@dotcom-tool-kit/npm": "4.0.0-beta.5", + "@dotcom-tool-kit/serverless": "3.0.0-beta.5" }, "repository": { "type": "git", @@ -20,10 +20,10 @@ "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/backend-serverless-app", "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/circleci-deploy/.toolkitrc.yml b/plugins/circleci-deploy/.toolkitrc.yml index 067c53577..a3bf94d7c 100644 --- a/plugins/circleci-deploy/.toolkitrc.yml +++ b/plugins/circleci-deploy/.toolkitrc.yml @@ -1,2 +1,88 @@ plugins: - '@dotcom-tool-kit/circleci' + +options: + hooks: + - CircleCi: + jobs: + - name: deploy-review + command: 'deploy:review' + - name: deploy-staging + command: 'deploy:staging' + - name: e2e-test-review + command: 'test:review' + - name: e2e-test-staging + command: 'test:staging' + - name: deploy-production + command: 'deploy:production' + workflows: + - name: 'tool-kit' + jobs: + - name: 'deploy-review' + requires: + - 'setup' + - 'waiting-for-approval' + splitIntoMatrix: false + custom: + filters: + branches: + ignore: main + - name: 'deploy-staging' + requires: + - 'setup' + splitIntoMatrix: false + custom: + filters: + branches: + only: main + - name: 'e2e-test-review' + requires: + - 'deploy-review' + splitIntoMatrix: false + custom: + !toolkit/if-defined '@dotcom-tool-kit/circleci.cypressImage': + executor: cypress + !toolkit/if-defined '@dotcom-tool-kit/serverless.awsAccountId': + aws-account-id: !toolkit/option '@dotcom-tool-kit/serverless.awsAccountId' + system-code: !toolkit/option '@dotcom-tool-kit/serverless.systemCode' + !toolkit/if-defined '@dotcom-tool-kit/next-router.appName': + appName: !toolkit/option '@dotcom-tool-kit/next-router.appName' + - name: 'e2e-test-staging' + splitIntoMatrix: false + requires: + - 'deploy-staging' + custom: + !toolkit/if-defined '@dotcom-tool-kit/circleci.cypressImage': + executor: cypress + - name: 'deploy-production' + requires: + - 'test' + - 'e2e-test-staging' + splitIntoMatrix: false + custom: + filters: + branches: + only: main + !toolkit/if-defined '@dotcom-tool-kit/serverless.awsAccountId': + aws-account-id: !toolkit/option '@dotcom-tool-kit/serverless.awsAccountId' + system-code: !toolkit/option '@dotcom-tool-kit/serverless.systemCode' + - name: 'nightly' + jobs: + - name: 'deploy-review' + requires: + - 'setup' + - 'waiting-for-approval' + splitIntoMatrix: false + custom: + filters: + branches: + ignore: main + !toolkit/if-defined '@dotcom-tool-kit/serverless.awsAccountId': + aws-account-id: !toolkit/option '@dotcom-tool-kit/serverless.awsAccountId' + system-code: !toolkit/option '@dotcom-tool-kit/serverless.systemCode' + !toolkit/if-defined '@dotcom-tool-kit/circleci.cypressImage': + executors: + - name: cypress + image: !toolkit/option '@dotcom-tool-kit/circleci.cypressImage' + +version: 2 diff --git a/lib/package-json-hook/.toolkitrc.yml b/plugins/circleci-deploy/index.js similarity index 100% rename from lib/package-json-hook/.toolkitrc.yml rename to plugins/circleci-deploy/index.js diff --git a/plugins/circleci-deploy/jest.config.js b/plugins/circleci-deploy/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/circleci-deploy/jest.config.js +++ b/plugins/circleci-deploy/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/circleci-deploy/package.json b/plugins/circleci-deploy/package.json index a42e6f844..2d6805edb 100644 --- a/plugins/circleci-deploy/package.json +++ b/plugins/circleci-deploy/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/circleci-deploy", - "version": "3.4.3", + "version": "4.0.0-beta.6", "description": "", "main": "lib", "scripts": { @@ -10,7 +10,7 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/circleci": "^6.0.1", + "@dotcom-tool-kit/circleci": "7.0.0-beta.6", "tslib": "^2.3.1" }, "repository": { @@ -31,10 +31,10 @@ "winston": "^3.5.1" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/circleci-deploy/src/index.ts b/plugins/circleci-deploy/src/index.ts deleted file mode 100644 index 50548ef15..000000000 --- a/plugins/circleci-deploy/src/index.ts +++ /dev/null @@ -1,144 +0,0 @@ -import CircleCiConfigHook, { - CircleCIStatePartial, - generateConfigWithJob -} from '@dotcom-tool-kit/circleci/lib/circleci-config' -import { TestCI } from '@dotcom-tool-kit/circleci/lib/index' -import { getOptions } from '@dotcom-tool-kit/options' -import { JobConfig } from '@dotcom-tool-kit/types/src/circleci' -import type { Logger } from 'winston' - -const getServerlessAdditionalFields = (logger: Logger): JobConfig => { - const serverlessOptions = getOptions('@dotcom-tool-kit/serverless') - const herokuOptions = getOptions('@dotcom-tool-kit/heroku') - - if (serverlessOptions && herokuOptions) { - logger.warn( - 'Tool Kit currently does not support managing Heroku and Serverless apps in the same project.' - ) - } - - if (!serverlessOptions?.awsAccountId || !serverlessOptions?.systemCode) { - return {} - } - - return { - 'aws-account-id': serverlessOptions.awsAccountId, - 'system-code': serverlessOptions.systemCode - } -} - -export class DeployReview extends CircleCiConfigHook { - static job = 'tool-kit/deploy-review' - // needs to be a getter so that we can lazily wait for the global options - // object to be assigned before getting values from it - get config(): CircleCIStatePartial { - // CircleCI config generator which will additionally optionally pass Serverless - // options as parameters to the orb job to enable OIDC authentication - const serverlessAdditionalsFields = getServerlessAdditionalFields(this.logger) - - return generateConfigWithJob({ - name: DeployReview.job, - addToNightly: true, - requires: ['tool-kit/setup', 'waiting-for-approval'], - splitIntoMatrix: false, - additionalFields: { filters: { branches: { ignore: 'main' } }, ...serverlessAdditionalsFields } - }) - } -} - -export class DeployStaging extends CircleCiConfigHook { - static job = 'tool-kit/deploy-staging' - get config(): CircleCIStatePartial { - return generateConfigWithJob({ - name: DeployStaging.job, - addToNightly: false, - requires: ['tool-kit/setup'], - splitIntoMatrix: false, - additionalFields: { filters: { branches: { only: 'main' } } } - }) - } -} - -export class TestReview extends CircleCiConfigHook { - static job = 'tool-kit/e2e-test-review' - - get config() { - const jobOptions = { - name: TestReview.job, - requires: [DeployReview.job], - addToNightly: false, - splitIntoMatrix: false - } - - // CircleCI config generator which will additionally optionally pass Serverless - // options as parameters to the orb job to enable OIDC authentication - const serverlessAdditionalsFields = getServerlessAdditionalFields(this.logger) - - const options = getOptions('@dotcom-tool-kit/circleci') - if (options?.cypressImage) { - return { - executors: { cypress: { docker: [{ image: options.cypressImage }] } }, - ...generateConfigWithJob({ - ...jobOptions, - additionalFields: { ...serverlessAdditionalsFields, executor: 'cypress' } - }) - } - } - return generateConfigWithJob({ ...jobOptions, additionalFields: serverlessAdditionalsFields }) - } -} - -export class TestStaging extends CircleCiConfigHook { - static job = 'tool-kit/e2e-test-staging' - - get config() { - const jobOptions = { - name: TestStaging.job, - requires: [DeployStaging.job], - addToNightly: false, - splitIntoMatrix: false - } - - const options = getOptions('@dotcom-tool-kit/circleci') - if (options?.cypressImage) { - return { - executors: { cypress: { docker: [{ image: options.cypressImage }] } }, - ...generateConfigWithJob({ - ...jobOptions, - additionalFields: { executor: 'cypress' } - }) - } - } - return generateConfigWithJob(jobOptions) - } -} - -export class DeployProduction extends CircleCiConfigHook { - static job = 'tool-kit/deploy-production' - get config(): CircleCIStatePartial { - // CircleCI config generator which will additionally optionally pass Serverless - // options as parameters to the orb job to enable OIDC authentication - const serverlessAdditionalsFields = getServerlessAdditionalFields(this.logger) - - return generateConfigWithJob({ - name: DeployProduction.job, - addToNightly: false, - requires: [TestStaging.job, TestCI.job], - splitIntoMatrix: false, - additionalFields: { - ...serverlessAdditionalsFields, - filters: { branches: { only: 'main' } } - } - }) - } -} - -export const hooks = { - 'deploy:review': DeployReview, - 'deploy:staging': DeployStaging, - 'test:review': TestReview, - 'teardown:review': TestReview, - 'test:staging': TestStaging, - 'teardown:staging': TestStaging, - 'deploy:production': DeployProduction -} diff --git a/plugins/circleci-deploy/test/index.test.ts b/plugins/circleci-deploy/test/index.test.ts deleted file mode 100644 index fc1fb9e62..000000000 --- a/plugins/circleci-deploy/test/index.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { describe, it, expect } from '@jest/globals' -import * as circleciDeploy from '../' - -describe('CircleCI-Deploy plugin', () => { - it('should define CI deployment hooks', () => { - expect(circleciDeploy.hooks).toEqual( - expect.objectContaining({ - 'deploy:review': expect.any(Function), - 'deploy:staging': expect.any(Function), - 'deploy:production': expect.any(Function), - 'teardown:review': expect.any(Function), - 'teardown:staging': expect.any(Function), - 'test:review': expect.any(Function), - 'test:staging': expect.any(Function) - }) - ) - }) -}) diff --git a/plugins/circleci-deploy/tsconfig.json b/plugins/circleci-deploy/tsconfig.json index 853368ad2..e5f0bccf7 100644 --- a/plugins/circleci-deploy/tsconfig.json +++ b/plugins/circleci-deploy/tsconfig.json @@ -6,7 +6,7 @@ }, "references": [ { - "path": "../../lib/types" + "path": "../../lib/base" }, { "path": "../../lib/options" @@ -15,5 +15,7 @@ "path": "../circleci" } ], - "include": ["src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/plugins/circleci-heroku/.toolkitrc.yml b/plugins/circleci-heroku/.toolkitrc.yml index 2eee988b1..332016f76 100644 --- a/plugins/circleci-heroku/.toolkitrc.yml +++ b/plugins/circleci-heroku/.toolkitrc.yml @@ -2,8 +2,10 @@ plugins: - '@dotcom-tool-kit/circleci-deploy' - '@dotcom-tool-kit/heroku' -hooks: +commands: 'deploy:review': HerokuReview 'deploy:staging': HerokuStaging 'deploy:production': HerokuProduction 'teardown:staging': HerokuTeardown + +version: 2 diff --git a/plugins/circleci-heroku/index.js b/plugins/circleci-heroku/index.js index c32ed7534..e69de29bb 100644 --- a/plugins/circleci-heroku/index.js +++ b/plugins/circleci-heroku/index.js @@ -1 +0,0 @@ -exports.tasks = [] diff --git a/plugins/circleci-heroku/package.json b/plugins/circleci-heroku/package.json index a63cb3cb3..352faf90c 100644 --- a/plugins/circleci-heroku/package.json +++ b/plugins/circleci-heroku/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/circleci-heroku", - "version": "3.2.4", + "version": "4.0.0-beta.6", "description": "", "main": "index.js", "scripts": { @@ -10,8 +10,8 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/circleci-deploy": "^3.4.3", - "@dotcom-tool-kit/heroku": "^3.4.1", + "@dotcom-tool-kit/circleci-deploy": "4.0.0-beta.6", + "@dotcom-tool-kit/heroku": "4.0.0-beta.5", "tslib": "^2.3.1" }, "repository": { @@ -25,10 +25,10 @@ "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/circleci-npm/.toolkitrc.yml b/plugins/circleci-npm/.toolkitrc.yml index 3cbf26a25..4e3d3d009 100644 --- a/plugins/circleci-npm/.toolkitrc.yml +++ b/plugins/circleci-npm/.toolkitrc.yml @@ -2,5 +2,26 @@ plugins: - '@dotcom-tool-kit/circleci' - '@dotcom-tool-kit/npm' -hooks: - 'publish:tag': NpmPublish \ No newline at end of file +commands: + 'publish:tag': NpmPublish + +options: + hooks: + - CircleCi: + jobs: + - name: publish-tag + command: 'publish:tag' + workflows: + - name: 'tool-kit' + jobs: + - name: 'publish-tag' + requires: + - 'test' + splitIntoMatrix: true + custom: + context: 'npm-publish-token' + filters: + branches: + ignore: '/.*/' + +version: 2 diff --git a/plugins/circleci-npm/index.js b/plugins/circleci-npm/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/circleci-npm/jest.config.js b/plugins/circleci-npm/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/circleci-npm/jest.config.js +++ b/plugins/circleci-npm/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/circleci-npm/package.json b/plugins/circleci-npm/package.json index 05943cc65..c9b55b647 100644 --- a/plugins/circleci-npm/package.json +++ b/plugins/circleci-npm/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/circleci-npm", - "version": "5.3.3", + "version": "6.0.0-beta.6", "description": "", "main": "lib", "scripts": { @@ -10,9 +10,8 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/types": "^3.6.0", - "@dotcom-tool-kit/circleci": "^6.0.1", - "@dotcom-tool-kit/npm": "^3.3.1", + "@dotcom-tool-kit/circleci": "7.0.0-beta.6", + "@dotcom-tool-kit/npm": "4.0.0-beta.5", "tslib": "^2.3.1" }, "repository": { @@ -27,10 +26,10 @@ ".toolkitrc.yml" ], "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/circleci-npm/src/index.ts b/plugins/circleci-npm/src/index.ts deleted file mode 100644 index 639fcd3bd..000000000 --- a/plugins/circleci-npm/src/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -import CircleCiConfigHook, { generateConfigWithJob } from '@dotcom-tool-kit/circleci/lib/circleci-config' - -class PublishHook extends CircleCiConfigHook { - static job = 'tool-kit/publish-tag' - config = generateConfigWithJob({ - name: PublishHook.job, - requires: ['tool-kit/test'], - splitIntoMatrix: false, - addToNightly: false, - additionalFields: { - context: 'npm-publish-token', - filters: { - branches: { ignore: '/.*/' } - } - } - }) -} - -export const hooks = { - 'publish:tag': PublishHook -} diff --git a/plugins/circleci-npm/test/index.test.ts b/plugins/circleci-npm/test/index.test.ts deleted file mode 100644 index eeb843176..000000000 --- a/plugins/circleci-npm/test/index.test.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, it, expect } from '@jest/globals' -import { hooks } from '../' - -describe('npm plugin', () => { - it('should define package.json hooks', () => { - expect(hooks).toEqual( - expect.objectContaining({ - 'publish:tag': expect.any(Function) - }) - ) - }) -}) diff --git a/plugins/circleci-npm/tsconfig.json b/plugins/circleci-npm/tsconfig.json index 1ce32d331..5428d7010 100644 --- a/plugins/circleci-npm/tsconfig.json +++ b/plugins/circleci-npm/tsconfig.json @@ -6,7 +6,7 @@ }, "references": [ { - "path": "../../lib/types" + "path": "../../lib/base" }, { "path": "../circleci" @@ -18,4 +18,4 @@ "include": [ "src/**/*" ] -} \ No newline at end of file +} diff --git a/plugins/circleci/.toolkitrc.yml b/plugins/circleci/.toolkitrc.yml index e69de29bb..ba83dabee 100644 --- a/plugins/circleci/.toolkitrc.yml +++ b/plugins/circleci/.toolkitrc.yml @@ -0,0 +1,37 @@ +installs: + CircleCi: + entryPoint: './lib/circleci-config' + managesFiles: + - '.circleci/config.yml' + +options: + hooks: + - CircleCi: + jobs: + - name: build + command: 'build:ci' + - name: test + command: 'test:ci' + workflows: + - name: 'tool-kit' + jobs: + - name: build + requires: + - 'setup' + - name: test + requires: + - 'build' + runOnRelease: true + - name: 'nightly' + jobs: + - name: build + requires: + - 'setup' + - name: test + requires: + - 'build' + +init: + - './lib/init-env-vars' + +version: 2 diff --git a/plugins/circleci/jest.config.js b/plugins/circleci/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/circleci/jest.config.js +++ b/plugins/circleci/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index a0d032ba2..dc604d06b 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,16 +1,18 @@ { "name": "@dotcom-tool-kit/circleci", - "version": "6.0.1", + "version": "7.0.0-beta.6", "description": "", "main": "lib", "scripts": { "test": "cd ../../ ; npx jest --silent --projects plugins/circleci" }, "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/state": "^3.3.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/conflict": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "jest-diff": "^29.5.0", "lodash": "^4.17.21", "tslib": "^2.3.1", @@ -28,11 +30,14 @@ "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/circleci", "devDependencies": { + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "@types/jest": "^27.4.0", "@types/js-yaml": "^4.0.3", "@types/lodash": "^4.14.185", - "winston": "^3.5.1" + "winston": "^3.5.1", + "zod": "^3.22.4" }, "files": [ "/lib", @@ -42,10 +47,11 @@ "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5", + "zod": "^3.22.4" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/circleci/readme.md b/plugins/circleci/readme.md index 7cf470741..4fbdbc45f 100644 --- a/plugins/circleci/readme.md +++ b/plugins/circleci/readme.md @@ -1,6 +1,6 @@ # @dotcom-tool-kit/circleci -This plugin exposes state from the CircleCI environment for other plugins to consume generically. It also manages Tool Kit hooks that are run from CircleCI workflows. +This plugin manages Tool Kit commands that are run from CircleCI workflows, via a Tool Kit [`Hook`](#hooks) that automatically manages `.circleci/config.yml`. It also exposes state from the CircleCI environment for other plugins to consume generically. This plugin will be installed as a dependency of the [frontend-app](https://github.com/Financial-Times/dotcom-tool-kit/tree/main/plugins/frontend-app), [backend-heroku-app](https://github.com/Financial-Times/dotcom-tool-kit/tree/main/plugins/backend-heroku-app), [component](https://github.com/Financial-Times/dotcom-tool-kit/tree/main/plugins/component), [circleci-deploy](https://github.com/Financial-Times/dotcom-tool-kit/tree/main/plugins/circleci-deploy), and [circleci-npm](https://github.com/Financial-Times/dotcom-tool-kit/tree/main/plugins/circleci-npm) plugins so you do not need to install it separately if you are using any of those plugins. @@ -25,16 +25,35 @@ And install this plugin's hooks: npx dotcom-tool-kit --install ``` -## Options + +## Hooks -| Key | Description | Default value | -| ------------- | --------------------------------------- | ------------- | -| `nodeVersion` | The Node versioned docker image tag for CircleCI to use. Any [CircleCI image tag](https://circleci.com/developer/images/image/cimg/node#image-tags) is valid, for example `18.16-browsers` or `16.14`. Can be an array of versions to create a matrix pipeline. The first version in the list is what will be used for publishing etc. | `16.14-browsers` | +### `CircleCi` -## Hooks +This hook automatically manages `.circleci/config.yml` in your repo to provide configuration for CircleCI workflows to run Tool Kit commands and tasks. + +Options provided in your repository's `.toolkitrc.yml` for this hook are merged with any Tool Kit plugin that also provides options for the hook. + +Unless they conflict, your options are appended to options from plugins, allowing you to define custom CircleCI jobs and workflows in your repository that work alongside those from plugins. +#### Hook options + +| Property | Description | Type | +| :------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `executors` | an array of additional CircleCI executors to output in the generated config. | _Array of objects:_
  • `name`: `string`
  • `image`: `string`
| +| `jobs` | an array of additional CircleCI jobs to output in the generated config. these are used for running Tool Kit commands. for running arbitrary shell commands, use `custom`. | _Array of objects:_
  • `name`: `string`
  • `command`: `string`
| +| `workflows` | an array of additional CircleCI workflows to output in the generated config. these reference jobs defined in the `jobs` option. | _Array of objects:_
  • `name`: `string`
  • `jobs`: _Array of objects:_
    • `name`: `string`
    • `requires`: `Array`
    • `splitIntoMatrix`: `boolean`
    • `custom`: `unknown` (_nullable_)
  • `runOnRelease`: `boolean`
  • `custom`: `unknown` (_nullable_)
| +| `custom` | arbitrary additional CircleCI configuration that will be merged into the Tool Kit-generated config. | _Object with dynamic keys of type_ `string` _and values of type_ `unknown` (_optional & nullable_) | +| `disableBaseConfig` | set to `true` to omit the Tool Kit CircleCI boilerplate. should be used along with `custom` to provide your own boilerplate. | `boolean` | + +_All properties are optional._ + +## Plugin-wide options + +### `@dotcom-tool-kit/circleci` + +| Property | Description | Type | Default | +| :----------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------- | :------------------- | +| `cimgNodeVersions` | list of CircleCI [Node.js image versions](https://circleci.com/developer/images/image/cimg/node) to use. if more than one is provided, a [matrix build](https://circleci.com/docs/using-matrix-jobs/) will be generated in your CircleCI config. | `Array` | `["18.19-browsers"]` | -| Event | Description | Installed to... | Default Hooks | -| ------------- | -------------------------------------------------------- | -------------------------------------- | -------------------------------------- | -| `build:ci` | Compile any assets or code required for your app to run. | `build` job in `.circle/config.yml` | `WebpackProduction`, `BabelProduction` | -| `test:ci` | Run your app's test suite. | `test` job in `.circle/config.yml` | `Eslint`, `Mocha`, `JestCi` | -| `test:review` | Run your app's E2E tests against a deployed Review App. | `e2e-test` job in `.circle/config.yml` | `NTest`, `Pa11y` | +_All properties are optional._ + diff --git a/plugins/circleci/src/circleci-config.ts b/plugins/circleci/src/circleci-config.ts index f46b6c51d..c8171d8a1 100644 --- a/plugins/circleci/src/circleci-config.ts +++ b/plugins/circleci/src/circleci-config.ts @@ -1,20 +1,70 @@ +import type { + CircleCiOptions, + CircleCiSchema, + CircleCiWorkflow +} from '@dotcom-tool-kit/schemas/lib/hooks/circleci' +import { type Conflict, isConflict } from '@dotcom-tool-kit/conflict' +import { Hook, type HookInstallation } from '@dotcom-tool-kit/base' +import { type Plugin } from '@dotcom-tool-kit/plugin' import { ToolKitError } from '@dotcom-tool-kit/error' import { styles } from '@dotcom-tool-kit/logger' import { getOptions } from '@dotcom-tool-kit/options' -import { Hook } from '@dotcom-tool-kit/types' -import { automatedComment, CircleConfig, Job, JobConfig, Workflow } from '@dotcom-tool-kit/types/lib/circleci' -import { semVerRegex } from '@dotcom-tool-kit/types/lib/npm' import { promises as fs } from 'fs' import { diffStringsUnified } from 'jest-diff' +import groupBy from 'lodash/groupBy' import isPlainObject from 'lodash/isPlainObject' import isMatch from 'lodash/isMatch' import merge from 'lodash/merge' import mergeWith from 'lodash/mergeWith' -import omit from 'lodash/omit' import path from 'path' +import partition from 'lodash/partition' import type { PartialDeep } from 'type-fest' import YAML from 'yaml' +const automatedComment = '# CONFIG GENERATED BY DOTCOM-TOOL-KIT, DO NOT EDIT BY HAND\n' + +type JobConfig = { + type?: string + docker?: { image: string; environment?: Record }[] + context?: string | string[] + requires?: string[] + filters?: { branches?: { only?: string; ignore?: string }; tags?: { only?: string } } + executor?: string + [parameter: string]: unknown +} + +type Step = Record + +type Job = string | { [job: string]: JobConfig } + +// TODO:20240410:IM rethink this whole type, it's very fly-by-night at the +// moment and constantly requires updates whenever we change the code +interface CircleConfig { + version: 2.1 + orbs: { + [orb: string]: string + } + executors: { + [executor: string]: { + docker: { image: string }[] + } + } + jobs: { + [job: string]: { + docker?: { image: string }[] + executor?: string + parameters?: unknown + steps: (string | { [command: string]: Step })[] + } + } + workflows: { + [workflow: string]: { + when: unknown + jobs: Job[] + } + } +} + const MAJOR_ORB_VERSION = '5' export type CircleCIState = CircleConfig @@ -34,8 +84,7 @@ const getNodeVersions = (): Array => { // empty string. The first executor is named 'node' without any reference to // the version so the plugins which don't support matrices don't need to know // the version option. - const nodeVersion = getOptions('@dotcom-tool-kit/circleci')?.nodeVersion ?? '' - return Array.isArray(nodeVersion) ? nodeVersion : [nodeVersion] + return getOptions('@dotcom-tool-kit/circleci')?.cimgNodeVersions ?? [''] } /* Applies a verion identifier for all but the first (and therefore default) @@ -44,23 +93,8 @@ const getNodeVersions = (): Array => { const nodeVersionToExecutor = (version: string, index: number): string => index === 0 ? 'node' : `node${version.replaceAll('.', '_')}` -// These boilerplate objects are (typically) needed for each job. They can be -// spread into your custom config, and are automatically included when calling -// generateSimpleJob. - -/** - * Every Tool Kit job uses a Node executor. We define a list of possible Node - * executors at the top of the CircleCI config, and jobs can either opt for the - * default executor (shortened to just 'node') with `nightlyBoilerplate` or to - * run with all the different executors in a matrix via `matrixBoilerplate`. - * version of Node to use. - */ -export const nightlyBoilerplate = { - executor: 'node' -} -// Needs to be lazy as the node versions haven't been loaded yet when this -// module is initialised. -export const matrixBoilerplate = () => ({ +const matrixBoilerplate = (jobName: string) => ({ + name: `${jobName}-<< matrix.executor >>`, matrix: { parameters: { executor: getNodeVersions().map(nodeVersionToExecutor) @@ -72,83 +106,20 @@ export const matrixBoilerplate = () => ({ * tagFilter sets the regex for GitHub release tags: CircleCI will ignore jobs * when doing a release if the filter isn't made explicit */ -export const tagFilter = { filters: { tags: { only: `${semVerRegex}` } } } -/** - * @deprecated explicitly using each of the objects this spreads is preferred. - * jobBoilerplate is the config needed for all Tool Kit jobs in the `tool-kit` - * workflow, and combines the `nightlyBoilerplate` and `tagFilter` objects. - */ -export const jobBoilerplate = { - ...nightlyBoilerplate, - ...tagFilter -} +export const tagFilter = { filters: { tags: { only: `${/^v\d+\.\d+\.\d+(-.+)?/}` } } } -export interface JobGeneratorOptions { - name: string - /** whether to include in `nightly` workflow or just `tool-kit` */ - addToNightly: boolean - requires: string[] - /** whether this job can be run multiple times with different Node versions */ - splitIntoMatrix: boolean - /** other fields to include in the job */ - additionalFields?: JobConfig -} - -/** - * `generateConfigWithJob` generates a single job, structured so that it will - * merge nicely with the rest of the config. This will include the `requires` - * parameter, as well as the boilerplate properties from `matrixBoilerplate`, - * but any other options will need to be passed to `additionalFields`, such as - * `filters.branches`. - */ -export const generateConfigWithJob = (options: JobGeneratorOptions): CircleCIStatePartial => { - const jobBase = options.splitIntoMatrix - ? { - name: `${options.name}-<< matrix.executor >>`, - requires: options.requires.map((dep) => - dep === 'waiting-for-approval' ? dep : `${dep}-<< matrix.executor >>` - ), - ...matrixBoilerplate() - } - : { - // only require the latest Node version of a matrix job in order to - // avoid workspace conflicts - requires: options.requires.map((dep) => (dep === 'waiting-for-approval' ? dep : `${dep}-node`)), - // append the default executor name to the job name so that multiple - // non-matrix jobs can be chained one after another without having to - // know whether a matrix job precedes them or not - name: `${options.name}-node`, - ...nightlyBoilerplate - } - const config: CircleCIStatePartial = { - workflows: { - 'tool-kit': { - jobs: [ - { - // avoid overwriting the jobBase variable - [options.name]: merge({}, jobBase, tagFilter, options.additionalFields) - } - ] - } - } - } - if (options.addToNightly) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - config.workflows!.nightly = { - jobs: [ - { - [options.name]: merge( - { ...jobBase, requires: jobBase.requires.filter((dep) => dep !== 'waiting-for-approval') }, - omit(options.additionalFields, ['filters']) - ) - } - ] +// helper override to the Lodash mergeWith function with a pre-defined +// customiser that will concatenate arrays rather than overriding them by index +const mergeWithConcatenatedArrays = (arg0: unknown, ...args: unknown[]) => + mergeWith(arg0, ...args, (obj: unknown, source: unknown) => { + if (Array.isArray(obj)) { + return obj.concat(source) } - } - return config -} + }) -const getInitialState = (): CircleCIState => { +const getBaseConfig = (): CircleCIState => { + const runsOnMultipleNodeVersions = getNodeVersions().length > 1 + const setupMatrix = runsOnMultipleNodeVersions ? matrixBoilerplate('tool-kit/setup') : { executor: 'node' } return { version: 2.1, orbs: { @@ -194,9 +165,8 @@ const getInitialState = (): CircleCIState => { }, { 'tool-kit/setup': { - name: 'tool-kit/setup-<< matrix.executor >>', + ...setupMatrix, requires: ['checkout', 'waiting-for-approval'], - ...matrixBoilerplate(), ...tagFilter } } @@ -213,48 +183,175 @@ const getInitialState = (): CircleCIState => { } ] }, - jobs: [ - 'checkout', - { - 'tool-kit/setup': { - name: 'tool-kit/setup-<< matrix.executor >>', - requires: ['checkout'], - ...matrixBoilerplate() - } - } - ] + jobs: ['checkout', { 'tool-kit/setup': { ...setupMatrix, requires: ['checkout'] } }] } } } } +const rootOptionKeys = ['executors', 'jobs', 'workflows'] as const satisfies readonly (keyof Omit< + CircleCiOptions, + 'custom' +>)[] + const isAutomatedConfig = (config: string): boolean => config.startsWith(automatedComment) const isNotToolKitConfig = (config: string): boolean => !config.includes('tool-kit') -const getJobName = (job: Job): string => (typeof job === 'string' ? job : Object.keys(job)[0]) +const isObject = (val: unknown): val is Record => isPlainObject(val) -const hasJob = (expectedJob: string, jobs: NonNullable): boolean => - jobs.some( - (job) => - (typeof job === 'string' && job === expectedJob) || - (typeof job === 'object' && job.hasOwnProperty(expectedJob)) +const customOptionsOverlap = ( + installation: Record, + other: Record +): boolean => + Object.entries(installation).some(([key, value]) => { + if (key in other) { + const otherVal = other[key] + if (isObject(value) && isObject(otherVal)) { + return customOptionsOverlap(value, otherVal) + } else if (Array.isArray(value) && Array.isArray(otherVal)) { + return false + } else { + return value !== otherVal + } + } else { + return false + } + }) + +const rootOptionOverlaps = (root: { name: string }[], other: { name: string }[]): boolean => { + const otherNames = other.map(({ name }) => name) + return root.map(({ name }) => name).some((name) => otherNames.includes(name)) +} + +const installationsOverlap = ( + installation: HookInstallation, + other: HookInstallation +): boolean => + customOptionsOverlap(installation.options?.custom ?? {}, other.options?.custom ?? {}) || + rootOptionKeys.some((rootOption) => + rootOptionOverlaps(installation.options?.[rootOption] ?? [], other.options?.[rootOption] ?? []) ) -export default abstract class CircleCiConfigHook extends Hook { - installGroup = 'circleci' +// classify installation as either mergeable or unmergeable, and mark any other +// installations that overlap with it as now unmergeable +const partitionInstallations = ( + installation: HookInstallation, + currentlyMergeable: HookInstallation[], + currentlyUnmergeable: HookInstallation[] +): [HookInstallation[], HookInstallation[]] => { + const [noLongerMergeable, mergeable] = partition(currentlyMergeable, (other) => + installationsOverlap(installation, other) + ) + const unmergeable = currentlyUnmergeable.concat(noLongerMergeable) + const overlapsWithUnmergeable = currentlyUnmergeable.some((other) => + installationsOverlap(installation, other) + ) + if (noLongerMergeable.length > 0 || overlapsWithUnmergeable) { + unmergeable.push(installation) + } else { + mergeable.push(installation) + } + + return [mergeable, unmergeable] +} + +// find any items with the same value in their 'name' field and merge those +// together +const mergeRootOptions = (options: T[]): T[] => + Object.values(groupBy(options, 'name')).map((matching) => mergeWithConcatenatedArrays({}, ...matching)) + +const mergeInstallations = (installations: HookInstallation[]): CircleCiOptions => ({ + // merge each of the root options ('executors', 'jobs', 'workflows') using + // their 'name' keys + ...Object.fromEntries( + rootOptionKeys.map((rootKey) => { + // flatten each installation's options into a single array (the order of + // the installations in the array is maintained) + const rootOptions = installations.flatMap<{ name: string }>( + (installation) => installation.options[rootKey] ?? [] + ) + return [rootKey, mergeRootOptions(rootOptions)] + }) + ), + // squash all the custom options together + custom: mergeWithConcatenatedArrays({}, ...installations.map((installation) => installation.options.custom)) +}) + +const mergeInstallationResults = ( + plugin: Plugin, + mergeable: HookInstallation[], + unmergeable: HookInstallation[] +) => { + const results: (HookInstallation | Conflict)[] = [] + + if (mergeable.length > 0) { + results.push({ + plugin, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: mergeInstallations(mergeable) + }) + } + + if (unmergeable.length > 0) { + results.push({ + plugin, + conflicting: unmergeable + }) + } + + return results +} + +const toolKitOrbPrefix = (job: string) => `tool-kit/${job}` + +const generateJobs = (workflow: CircleCiWorkflow): Job[] => { + const runsOnMultipleNodeVersions = getNodeVersions().length > 1 + return workflow.jobs.map((job) => { + const splitIntoMatrix = runsOnMultipleNodeVersions && (job.splitIntoMatrix ?? true) + return { + [toolKitOrbPrefix(job.name)]: merge( + splitIntoMatrix + ? matrixBoilerplate(toolKitOrbPrefix(job.name)) + : { + executor: 'node' + }, + { + requires: job.requires.map((required) => { + if (['checkout', 'waiting-for-approval'].includes(required)) { + return required + } + const requiredOrb = toolKitOrbPrefix(required) + // only need to include a suffix for the required job if it splits + // into a matrix for Node versions + const splitRequiredIntoMatrix = + runsOnMultipleNodeVersions && + (workflow.jobs?.find(({ name: jobName }) => required === jobName)?.splitIntoMatrix ?? true) + if (!splitRequiredIntoMatrix) { + return requiredOrb + } + return `${requiredOrb}-${splitIntoMatrix ? '<< matrix.executor >>' : 'node'}` + }) + }, + workflow.runOnRelease ? tagFilter : {}, + job.custom + ) + } + }) +} + +export default class CircleCi extends Hook { circleConfigPath = path.resolve(process.cwd(), '.circleci/config.yml') - _circleConfig?: string - haveCheckedBaseConfig = false - _versionTag?: string - abstract config: CircleCIStatePartial + private circleConfig?: string + private generatedConfig?: CircleCIState async getCircleConfig(): Promise { - if (!this._circleConfig) { + if (!this.circleConfig) { try { this.logger.verbose(`trying to read CircleCI config at ${styles.filepath(this.circleConfigPath)}...`) - this._circleConfig = await fs.readFile(this.circleConfigPath, 'utf8') + this.circleConfig = await fs.readFile(this.circleConfigPath, 'utf8') } catch (err) { // Not an error if config file doesn't exist if ((err as NodeJS.ErrnoException).code !== 'ENOENT') { @@ -263,39 +360,101 @@ export default abstract class CircleCiConfigHook extends Hook { } } - return this._circleConfig + return this.circleConfig + } + + static mergeChildInstallations( + plugin: Plugin, + childInstallations: (HookInstallation | Conflict>)[] + ): (HookInstallation | Conflict)[] { + const [mergeable, unmergeable] = childInstallations.reduce< + [HookInstallation[], HookInstallation[]] + >( + ([mergeable, unmergeable], installation) => { + // a conflicting installation is marked as unmergeable without + // affecting the mergeability of the other installations + if (isConflict(installation)) { + return [mergeable, [...unmergeable, ...installation.conflicting]] + } else { + return partitionInstallations(installation, mergeable, unmergeable) + } + }, + [[], []] + ) + + return mergeInstallationResults(plugin, mergeable, unmergeable) + } + + static overrideChildInstallations( + plugin: Plugin, + parentInstallation: HookInstallation, + childInstallations: (HookInstallation | Conflict>)[] + ): (HookInstallation | Conflict>)[] { + const mergeable: HookInstallation[] = [] + const unmergeable: HookInstallation[] = [] + + for (const installation of childInstallations) { + // TODO:IM there are multiple kinds of conflicts and this code currently + // assumes a parent resolving one conflict resolves them all + if (isConflict(installation)) { + const [canHandle, cannotHandle] = partition(installation.conflicting, (other) => + installationsOverlap(parentInstallation, other) + ) + + mergeable.push(...canHandle) + unmergeable.push(...cannotHandle) + } else { + mergeable.push(installation) + } + } + + mergeable.push(parentInstallation) + + return mergeInstallationResults(plugin, mergeable, unmergeable) } - async check(): Promise { + generateConfig(): CircleCIState { + if (!this.generatedConfig) { + const generated: CircleCIStatePartial = {} + if (this.options.executors) { + generated.executors = Object.fromEntries( + this.options.executors.map((executor) => [executor.name, { docker: [{ image: executor.image }] }]) + ) + } + if (this.options.workflows) { + generated.workflows = Object.fromEntries( + this.options.workflows.map((workflow) => { + const generatedJobs = { + jobs: generateJobs(workflow) + } + return [workflow.name, mergeWithConcatenatedArrays(generatedJobs, workflow.custom)] + }) + ) + } + this.generatedConfig = mergeWithConcatenatedArrays( + {}, + this.options.disableBaseConfig ? {} : getBaseConfig(), + generated, + this.options.custom ?? {} + ) + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.generatedConfig! + } + + async isInstalled(): Promise { const rawConfig = await this.getCircleConfig() if (!rawConfig) { return false } const config = YAML.parse(rawConfig) as CircleConfig - // only need to check that the base config matches once - if (!this.haveCheckedBaseConfig && !isMatch(config, getInitialState())) { - return false - } - this.haveCheckedBaseConfig = true - return isMatch(config, this.config) + const expectedConfig = this.generateConfig() + return isMatch(config, expectedConfig) } - async install(state?: CircleCIState): Promise { - if (!state) { - state = getInitialState() - } - // define a customiser function to make sure only jobs that aren't already - // listed are merged into the CircleCI config, and to force new jobs to be - // concatenated onto the array of other jobs rather than zipping them - // (i.e., overwriting the first few jobs in the array) - mergeWith(state, this.config, (prevState, newConfig, key) => { - if (key === 'jobs' && Array.isArray(prevState)) { - const uniqueJobs = newConfig.filter((job: Job) => !hasJob(getJobName(job), prevState)) - return prevState.concat(uniqueJobs) - } - }) - return state + async install(): Promise { + return this.generateConfig() } async commitInstall(state: CircleCIState): Promise { diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts deleted file mode 100644 index 9f15c88f7..000000000 --- a/plugins/circleci/src/index.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { writeState } from '@dotcom-tool-kit/state' -import CircleCiConfigHook, { generateConfigWithJob } from './circleci-config' - -export class BuildCI extends CircleCiConfigHook { - static job = 'tool-kit/build' - get config() { - return generateConfigWithJob({ - name: BuildCI.job, - requires: ['tool-kit/setup'], - splitIntoMatrix: true, - addToNightly: true - }) - } -} - -export class TestCI extends CircleCiConfigHook { - static job = 'tool-kit/test' - get config() { - return generateConfigWithJob({ - name: TestCI.job, - requires: [BuildCI.job], - splitIntoMatrix: true, - addToNightly: true - }) - } -} - -export const hooks = { - 'build:ci': BuildCI, - 'test:ci': TestCI -} - -const envVars = { - branch: process.env.CIRCLE_BRANCH, - repo: process.env.CIRCLE_PROJECT_REPONAME, - version: process.env.CIRCLE_SHA1, - tag: process.env.CIRCLE_TAG -} - -function pluginInit() { - if (process.env.CIRCLECI) { - /* eslint-disable-next-line no-console -- - * cannot use winston logging during module initialisation - **/ - console.log(`writing circle ci environment variables to state...`) - writeState('ci', envVars) - } -} - -pluginInit() diff --git a/plugins/circleci/src/init-env-vars.ts b/plugins/circleci/src/init-env-vars.ts new file mode 100644 index 000000000..81b611e16 --- /dev/null +++ b/plugins/circleci/src/init-env-vars.ts @@ -0,0 +1,19 @@ +import { writeState } from '@dotcom-tool-kit/state' +import { Init } from '@dotcom-tool-kit/base' + +export default class CircleCIEnvVars extends Init { + async init() { + const envVars = { + branch: process.env.CIRCLE_BRANCH, + repo: process.env.CIRCLE_PROJECT_REPONAME, + version: process.env.CIRCLE_SHA1, + tag: process.env.CIRCLE_TAG, + buildNumber: process.env.CIRCLE_BUILD_NUM + } + + if (process.env.CIRCLECI) { + this.logger.info(`writing circle ci environment variables to state...`) + writeState('ci', envVars) + } + } +} diff --git a/plugins/circleci/test/__snapshots__/circleci-config.test.ts.snap b/plugins/circleci/test/__snapshots__/circleci-config.test.ts.snap deleted file mode 100644 index 536edd37e..000000000 --- a/plugins/circleci/test/__snapshots__/circleci-config.test.ts.snap +++ /dev/null @@ -1,171 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`CircleCI config hook install correctly generates a new configuration file 1`] = ` -Object { - "executors": Object { - "node": Object { - "docker": Array [ - Object { - "image": "cimg/node:16.14-browsers", - }, - ], - }, - }, - "jobs": Object { - "checkout": Object { - "docker": Array [ - Object { - "image": "cimg/base:stable", - }, - ], - "steps": Array [ - "checkout", - Object { - "tool-kit/persist-workspace": Object { - "path": ".", - }, - }, - ], - }, - }, - "orbs": Object { - "tool-kit": "financial-times/dotcom-tool-kit@5", - }, - "version": 2.1, - "workflows": Object { - "nightly": Object { - "jobs": Array [ - "checkout", - Object { - "tool-kit/setup": Object { - "matrix": Object { - "parameters": Object { - "executor": Array [ - "node", - ], - }, - }, - "name": "tool-kit/setup-<< matrix.executor >>", - "requires": Array [ - "checkout", - ], - }, - }, - Object { - "test-job": Object { - "executor": "node", - "name": "test-job-node", - "requires": Array [ - "that-job-node", - ], - }, - }, - ], - "when": Object { - "and": Array [ - Object { - "equal": Array [ - "scheduled_pipeline", - "<< pipeline.trigger_source >>", - ], - }, - Object { - "equal": Array [ - "nightly", - "<< pipeline.schedule.name >>", - ], - }, - ], - }, - }, - "tool-kit": Object { - "jobs": Array [ - Object { - "checkout": Object { - "filters": Object { - "tags": Object { - "only": "/^v\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-.+)?/", - }, - }, - }, - }, - Object { - "waiting-for-approval": Object { - "filters": Object { - "branches": Object { - "only": "/(^renovate-.*|^nori/.*)/", - }, - }, - "type": "approval", - }, - }, - Object { - "tool-kit/setup": Object { - "filters": Object { - "tags": Object { - "only": "/^v\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-.+)?/", - }, - }, - "matrix": Object { - "parameters": Object { - "executor": Array [ - "node", - ], - }, - }, - "name": "tool-kit/setup-<< matrix.executor >>", - "requires": Array [ - "checkout", - "waiting-for-approval", - ], - }, - }, - Object { - "test-job": Object { - "executor": "node", - "filters": Object { - "tags": Object { - "only": "/^v\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-.+)?/", - }, - }, - "name": "test-job-node", - "requires": Array [ - "waiting-for-approval", - "that-job-node", - ], - }, - }, - Object { - "test-another-job": Object { - "filters": Object { - "tags": Object { - "only": "/^v\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-.+)?/", - }, - }, - "matrix": Object { - "parameters": Object { - "executor": Array [ - "node", - ], - }, - }, - "name": "test-another-job-<< matrix.executor >>", - "requires": Array [ - "waiting-for-approval", - "this-job-<< matrix.executor >>", - ], - }, - }, - ], - "when": Object { - "not": Object { - "equal": Array [ - "scheduled_pipeline", - "<< pipeline.trigger_source >>", - ], - }, - }, - }, - }, -} -`; diff --git a/plugins/circleci/test/circleci-config.test.ts b/plugins/circleci/test/circleci-config.test.ts index a194c60b1..355342de4 100644 --- a/plugins/circleci/test/circleci-config.test.ts +++ b/plugins/circleci/test/circleci-config.test.ts @@ -1,12 +1,19 @@ -import { setOptions } from '@dotcom-tool-kit/options/lib' +import { type HookInstallation } from '@dotcom-tool-kit/base' +import { setOptions } from '@dotcom-tool-kit/options' +import type { + CircleCiWorkflowJob, + CircleCiJob, + CircleCiOptions +} from '@dotcom-tool-kit/schemas/lib/hooks/circleci' import { describe, expect, it } from '@jest/globals' import fs from 'fs' import path from 'path' import winston, { Logger } from 'winston' import * as YAML from 'yaml' -import CircleCiConfigHook, { generateConfigWithJob } from '../src/circleci-config' -const logger = (winston as unknown) as Logger +import CircleCi from '../src/circleci-config' + +const logger = winston as unknown as Logger jest.mock('fs', () => { const originalModule = jest.requireActual('fs') @@ -18,67 +25,67 @@ jest.mock('fs', () => { }) const mockedWriteFile = jest.mocked(fs.promises.writeFile) -describe('CircleCI config hook', () => { - abstract class FakeCircleCiConfigHook extends CircleCiConfigHook { - // pretend it's never the first time we've opened the config so that we - // skip the comparison with the base config generated by getInitialState() - haveCheckedBaseConfig = true - } - class TestHook extends FakeCircleCiConfigHook { - config = generateConfigWithJob({ - name: 'test-job', - addToNightly: true, - requires: ['waiting-for-approval', 'that-job'], - splitIntoMatrix: false - }) - } - class TestAnotherHook extends FakeCircleCiConfigHook { - config = generateConfigWithJob({ - name: 'test-another-job', - addToNightly: false, - requires: ['waiting-for-approval', 'this-job'], - splitIntoMatrix: true - }) - } +const testJob: CircleCiJob = { + name: 'test-job', + command: 'test:local' +} +const testWorkflowJob: CircleCiWorkflowJob = { + name: 'test-job', + requires: ['waiting-for-approval', 'that-job'], + splitIntoMatrix: false +} +const overriddenTestJob: CircleCiJob = { ...testJob, command: 'test:override' } +const anotherTestJob: CircleCiJob = { + name: 'another-test-job', + command: 'test:ci' +} +const anotherTestWorkflowJob: CircleCiWorkflowJob = { + name: 'another-test-job', + requires: ['waiting-for-approval', 'this-job'], + splitIntoMatrix: true +} +const simpleOptions: CircleCiOptions = { + jobs: [testJob], + workflows: [{ name: 'tool-kit', jobs: [testWorkflowJob] }], + disableBaseConfig: true +} +describe('CircleCI config hook', () => { const originalDir = process.cwd() beforeAll(() => { // mirror the default options created by zod - setOptions('@dotcom-tool-kit/circleci', { nodeVersion: '16.14-browsers' }) + setOptions('@dotcom-tool-kit/circleci', { cimgNodeVersions: ['16.14-browsers'] }) }) afterEach(() => { process.chdir(originalDir) }) - describe('check', () => { + describe('isInstalled', () => { it('should return true if the hook job is in the circleci workflow', async () => { process.chdir(path.join(__dirname, 'files', 'with-hook')) - const hook = new TestHook(logger) - expect(await hook.check()).toBeTruthy() + const hook = new CircleCi(logger, 'CircleCi', simpleOptions) + expect(await hook.isInstalled()).toBeTruthy() }) it('should return false if the hook job is not in the circleci workflow', async () => { process.chdir(path.join(__dirname, 'files', 'without-hook')) - const hook = new TestHook(logger) - expect(await hook.check()).toBeFalsy() + const hook = new CircleCi(logger, 'CircleCi', simpleOptions) + expect(await hook.isInstalled()).toBeFalsy() }) it('should return false if the base configuration is missing', async () => { process.chdir(path.join(__dirname, 'files', 'with-hook')) - const hook = new TestHook(logger) - // reset field overridden by FakeCircleCiConfigHook so that we do check - // for the base config - hook.haveCheckedBaseConfig = false - expect(await hook.check()).toBeFalsy() + const hook = new CircleCi(logger, 'CircleCi', { ...simpleOptions, disableBaseConfig: false }) + expect(await hook.isInstalled()).toBeFalsy() }) }) describe('install', () => { it("should throw an error explaining how to autogenerate config if existing config file doesn't contain any tool-kit jobs", async () => { process.chdir(path.join(__dirname, 'files', 'without-tool-kit')) - const hook = new TestHook(logger) + const hook = new CircleCi(logger, 'CircleCi', simpleOptions) const state = await hook.install() await expect(hook.commitInstall(state)).rejects.toThrow( "Your project has an existing CircleCI config file which doesn't contain" @@ -87,7 +94,7 @@ describe('CircleCI config hook', () => { it('should throw an error explaining what to do if no autogenerated comment', async () => { process.chdir(path.join(__dirname, 'files', 'without-hook')) - const hook = new TestHook(logger) + const hook = new CircleCi(logger, 'CircleCi', simpleOptions) const state = await hook.install() await expect(hook.commitInstall(state)).rejects.toThrow( 'Your CircleCI configuration is missing the expected fields from Tool Kit:' @@ -97,112 +104,313 @@ describe('CircleCI config hook', () => { it("should add a job with its jobConfig to a file with a comment if it's not there", async () => { process.chdir(path.join(__dirname, 'files', 'comment-without-hook')) - const hook = new TestHook(logger) + const hook = new CircleCi(logger, 'CircleCi', simpleOptions) const state = await hook.install() await hook.commitInstall(state) const config = YAML.parse(mockedWriteFile.mock.calls[0][1] as string) expect(config).toEqual( expect.objectContaining({ - workflows: { + workflows: expect.objectContaining({ 'tool-kit': expect.objectContaining({ jobs: expect.arrayContaining([ expect.objectContaining({ - 'test-job': expect.objectContaining({ - requires: ['waiting-for-approval', 'that-job-node'] + 'tool-kit/test-job': expect.objectContaining({ + requires: ['waiting-for-approval', 'tool-kit/that-job'] }) }) ]) - }), - nightly: expect.objectContaining({ - jobs: expect.arrayContaining([ - { - 'test-job': expect.objectContaining({ - requires: ['that-job-node'] - }) - } - ]) }) - } + }) }) ) }) + }) - it('should merge jobs from two hooks', async () => { - process.chdir(path.join(__dirname, 'files', 'comment-without-hook')) + describe('conflict resolution', () => { + it('should merge children setting different fields', () => { + const childInstallations: HookInstallation[] = [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + executors: [ + { + name: 'test-executor', + image: 'cimg/node:16.19' + } + ] + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + jobs: [testJob] + } + }, + { + plugin: { id: 'c', root: 'plugins/c' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + workflows: [{ name: 'test-workflow', jobs: [testWorkflowJob], runOnRelease: true }] + } + } + ] + const plugin = { id: 'p', root: 'plugins/p' } - const hook = new TestHook(logger) - const anotherHook = new TestAnotherHook(logger) - let state = await hook.install() - state = await anotherHook.install(state) - await hook.commitInstall(state) + expect(CircleCi.mergeChildInstallations(plugin, childInstallations)).toEqual([ + { + plugin, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: expect.objectContaining({ + executors: [ + { + name: 'test-executor', + image: 'cimg/node:16.19' + } + ], + jobs: [testJob], + workflows: [{ name: 'test-workflow', jobs: [testWorkflowJob], runOnRelease: true }] + }) + } + ]) + }) - const config = YAML.parse(mockedWriteFile.mock.calls[0][1] as string) - expect(config).toEqual( - expect.objectContaining({ - workflows: { - 'tool-kit': expect.objectContaining({ - jobs: expect.arrayContaining([ - expect.objectContaining({ - 'test-job': expect.objectContaining({ - requires: ['waiting-for-approval', 'that-job-node'] - }) - }), - expect.objectContaining({ - 'test-another-job': expect.objectContaining({ - requires: ['waiting-for-approval', 'this-job-<< matrix.executor >>'] - }) - }) - ]) - }), - nightly: expect.objectContaining({ - jobs: expect.arrayContaining([ - { - 'test-job': expect.objectContaining({ - requires: ['that-job-node'] - }) - } - ]) - }) + it('should merge child overriding root options', () => { + const plugin = { id: 'p', root: 'plugins/p' } + const parentInstallation: HookInstallation = { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + jobs: [overriddenTestJob], + workflows: [{ name: 'test-workflow', jobs: [anotherTestWorkflowJob] }] + } + } + + const childInstallations: HookInstallation[] = [ + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + jobs: [testJob], + workflows: [{ name: 'test-workflow', jobs: [testWorkflowJob], runOnRelease: true }] } - }) - ) + } + ] + + expect(CircleCi.overrideChildInstallations(plugin, parentInstallation, childInstallations)).toEqual([ + { + plugin, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: expect.objectContaining({ + jobs: [overriddenTestJob], + workflows: [ + { name: 'test-workflow', jobs: [testWorkflowJob, anotherTestWorkflowJob], runOnRelease: true } + ] + }) + } + ]) }) - it('should discard job from duplicate hook', async () => { - process.chdir(path.join(__dirname, 'files', 'comment-without-hook')) + it('should merge sibling plugins setting the same field', () => { + const childInstallations: HookInstallation[] = [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + jobs: [testJob] + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + jobs: [anotherTestJob] + } + } + ] - const hook = new TestHook(logger) - const sameHook = new TestHook(logger) - let state = await hook.install() - state = await sameHook.install(state) - await hook.commitInstall(state) + const plugin = { id: 'p', root: 'plugins/p' } - const config = YAML.parse(mockedWriteFile.mock.calls[0][1] as string) - const partialExpectedJob = { - 'test-job': expect.objectContaining({ - requires: ['waiting-for-approval', 'that-job-node'] - }) - } - const { jobs } = config.workflows['tool-kit'] - expect(jobs).toHaveLength(4) - for (let i = 0; i < 3; i++) { - expect(jobs[i]).toEqual(expect.not.objectContaining(partialExpectedJob)) - } - expect(jobs[3]).toEqual(expect.objectContaining(partialExpectedJob)) + expect(CircleCi.mergeChildInstallations(plugin, childInstallations)).toEqual([ + { + plugin, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: expect.objectContaining({ + jobs: expect.arrayContaining([testJob, anotherTestJob]) + }) + } + ]) }) - it('correctly generates a new configuration file', async () => { - process.chdir(path.join(__dirname, 'files', 'comment-without-hook')) + it('should conflict sibling plugins setting the same job', () => { + const childInstallations: HookInstallation[] = [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + jobs: [testJob] + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + jobs: [overriddenTestJob] + } + } + ] - const hook = new TestHook(logger) - const anotherHook = new TestAnotherHook(logger) - let state = await hook.install() - state = await anotherHook.install(state) - await hook.commitInstall(state) + const plugin = { id: 'p', root: 'plugins/p' } - const config = YAML.parse(mockedWriteFile.mock.calls[0][1] as string) - expect(config).toMatchSnapshot() + expect(CircleCi.mergeChildInstallations(plugin, childInstallations)).toEqual([ + { + plugin, + conflicting: childInstallations + } + ]) + }) + + it('should conflict sibling plugins with the same custom field', () => { + const childInstallations: HookInstallation[] = [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + custom: { + version: '2.0' + } + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + custom: { + version: '2.1' + } + } + } + ] + + const plugin = { id: 'p', root: 'plugins/p' } + + expect(CircleCi.mergeChildInstallations(plugin, childInstallations)).toEqual([ + { + plugin, + conflicting: childInstallations + } + ]) + }) + + it('should conflict sibling plugins with the same deep custom field', () => { + const childInstallations: HookInstallation[] = [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + custom: { + parameters: { + 'test-param': { + type: 'string', + default: 'test' + } + } + } + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + custom: { + parameters: { + 'test-param': { + type: 'number', + default: 137 + } + } + } + } + } + ] + + const plugin = { id: 'p', root: 'plugins/p' } + + expect(CircleCi.mergeChildInstallations(plugin, childInstallations)).toEqual([ + { + plugin, + conflicting: childInstallations + } + ]) + }) + + it('should merge sibling plugins with different custom fields', () => { + const childInstallations: HookInstallation[] = [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + custom: { + version: '2.0' + } + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: { + custom: { + parameters: { + 'test-param': { + type: 'number', + default: 137 + } + } + } + } + } + ] + + const plugin = { id: 'p', root: 'plugins/p' } + + expect(CircleCi.mergeChildInstallations(plugin, childInstallations)).toEqual([ + { + plugin, + forHook: 'CircleCi', + hookConstructor: CircleCi, + options: expect.objectContaining({ + custom: { + version: '2.0', + parameters: { + 'test-param': { + type: 'number', + default: 137 + } + } + } + }) + } + ]) }) }) }) diff --git a/plugins/circleci/test/files/with-hook/.circleci/config.yml b/plugins/circleci/test/files/with-hook/.circleci/config.yml index ee711cdd8..63bea46ef 100644 --- a/plugins/circleci/test/files/with-hook/.circleci/config.yml +++ b/plugins/circleci/test/files/with-hook/.circleci/config.yml @@ -1,20 +1,8 @@ workflows: tool-kit: jobs: - - test-job: - name: test-job-node + - tool-kit/test-job: requires: - waiting-for-approval - - that-job-node - executor: node - filters: - tags: - only: /^v\d+\.\d+\.\d+(-.+)?/ - nightly: - jobs: - - test-job: - name: test-job-node - requires: - - waiting-for-approval - - that-job-node + - tool-kit/that-job executor: node diff --git a/plugins/circleci/tsconfig.json b/plugins/circleci/tsconfig.json index 9b0214cce..c92951d9d 100644 --- a/plugins/circleci/tsconfig.json +++ b/plugins/circleci/tsconfig.json @@ -2,19 +2,28 @@ "extends": "../../tsconfig.settings.json", "references": [ { - "path": "../../lib/error" + "path": "../../lib/base" }, { - "path": "../../lib/state" + "path": "../../lib/conflict" }, { - "path": "../../lib/logger" + "path": "../../lib/error" }, { - "path": "../../lib/types" + "path": "../../lib/logger" }, { "path": "../../lib/options" + }, + { + "path": "../../lib/plugin" + }, + { + "path": "../../lib/schemas" + }, + { + "path": "../../lib/state" } ], "compilerOptions": { diff --git a/plugins/component/.toolkitrc.yml b/plugins/component/.toolkitrc.yml index b01fd7dc6..a5419d57e 100644 --- a/plugins/component/.toolkitrc.yml +++ b/plugins/component/.toolkitrc.yml @@ -1,3 +1,5 @@ plugins: - '@dotcom-tool-kit/npm' - '@dotcom-tool-kit/circleci-npm' + +version: 2 diff --git a/plugins/component/index.js b/plugins/component/index.js index c32ed7534..e69de29bb 100644 --- a/plugins/component/index.js +++ b/plugins/component/index.js @@ -1 +0,0 @@ -exports.tasks = [] diff --git a/plugins/component/package.json b/plugins/component/package.json index 64b3d081b..fc3c7a41a 100644 --- a/plugins/component/package.json +++ b/plugins/component/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/component", - "version": "4.1.3", + "version": "5.0.0-beta.6", "description": "", "main": "index.js", "keywords": [], @@ -17,14 +17,14 @@ ".toolkitrc.yml" ], "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "dependencies": { - "@dotcom-tool-kit/circleci-npm": "^5.3.3", - "@dotcom-tool-kit/npm": "^3.3.1" + "@dotcom-tool-kit/circleci-npm": "6.0.0-beta.6", + "@dotcom-tool-kit/npm": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/component/tsconfig.json b/plugins/component/tsconfig.json index 318abdeb7..6dfb3f60a 100644 --- a/plugins/component/tsconfig.json +++ b/plugins/component/tsconfig.json @@ -6,8 +6,10 @@ }, "references": [ { - "path": "../../lib/types" + "path": "../../lib/base" } ], - "include": ["src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/plugins/cypress/.toolkitrc.yml b/plugins/cypress/.toolkitrc.yml index 3cdf0e6d6..2175ad660 100644 --- a/plugins/cypress/.toolkitrc.yml +++ b/plugins/cypress/.toolkitrc.yml @@ -1,5 +1,18 @@ -hooks: - 'test:review': CypressCi - 'test:staging': CypressCi - 'e2e:local': CypressLocal +plugins: + - '@dotcom-tool-kit/package-json-hook' +tasks: + Cypress: './lib/tasks/cypress' + +commands: + 'test:review': Cypress + 'test:staging': Cypress + 'e2e:local': Cypress + +options: + hooks: + - PackageJson: + scripts: + e2e-test: 'e2e:local' + +version: 2 diff --git a/plugins/cypress/package.json b/plugins/cypress/package.json index 45170d7d0..9285b2c03 100644 --- a/plugins/cypress/package.json +++ b/plugins/cypress/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/cypress", - "version": "4.0.1", + "version": "5.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -21,16 +21,20 @@ ".toolkitrc.yml" ], "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" }, "dependencies": { - "@dotcom-tool-kit/doppler": "^1.1.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/state": "^3.3.0", - "@dotcom-tool-kit/types": "^3.6.0" + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", + "@dotcom-tool-kit/state": "4.0.0-beta.0" + }, + "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0" } } diff --git a/plugins/cypress/readme.md b/plugins/cypress/readme.md index c30bb8407..80979cbee 100644 --- a/plugins/cypress/readme.md +++ b/plugins/cypress/readme.md @@ -19,25 +19,28 @@ plugins: ### Testing with Cypress locally -For local development, by default the `CypressLocal` task runs on the `e2e:local` hook. This hook is also defined within the `cypress` plugin and will install itself into your `package.json` config as the script `e2e-local`. Therefore, to run Cypress on a local instance of your project you just need to call `npm run e2e-local`. Note that, by default, this task does __not__ run your application for you, so if that's controlled by Tool Kit it's recommended you add its task to the `e2e:local` hook too. For example, your config could look like: +> [!IMPORTANT] +> Please check this documentation and make sure it's up to date with the way the Cypress plugin works now. + +For local development, by default the `CypressLocal` task runs on the `e2e:local` command. This command is also defined within the `cypress` plugin and will install itself into your `package.json` config as the script `e2e-local`. Therefore, to run Cypress on a local instance of your project you just need to call `npm run e2e-local`. Note that, by default, this task does __not__ run your application for you, so if that's controlled by Tool Kit it's recommended you add its task to the `e2e:local` command too. For example, your config could look like: ```yml plugins: - '@dotcom-tool-kit/cypress' - '@dotcom-tool-kit/node' -hooks: +command: 'e2e:local': - Node - - CypressLocal + - Cypress ``` ### Testing with Cypress on CI -The `CypressCI` task runs on the `test:review` and `test:staging` hooks by default. These will run your Cypress end-to-end tests against the currently deployed review or staging app respectively. +The `CypressCI` task runs on the `test:review` and `test:staging` commands by default. These will run your Cypress end-to-end tests against the currently deployed review or staging app respectively. -### Running on another hook -You can also configure Cypress to run on any other hook; for example, if you want to run it with `npm run test` via the `npm` plugin, you can manually configure Cypress to run on `npm`'s `test:local` hook: +### Running on another command +You can also configure Cypress to run on any other command; for example, if you want to run it with `npm run test` via the `npm` plugin, you can manually configure Cypress to run on `npm`'s `test:local` command: ```yml plugins: @@ -45,27 +48,22 @@ plugins: - '@dotcom-tool-kit/node' - '@dotcom-tool-kit/npm' -hooks: +command: 'test:local': - Node - - CypressLocal + - Cypress ``` + +## Tasks -## Options - -| Key | Description | Default value | -|-|-|-| -| `localUrl` | URL for Cypress to connect to for local tests | _required (if using `CypressLocal`)_ | - -## Hooks +### `Cypress` -| Event | Description | Installed to...| Default Tasks -|-|-|-|-| -| `e2e:local` | Run end-to-end tests locally | `e2e-local` job in `package.json` | `CypressLocal` | +Run Cypress end-to-end tests +#### Task options -## Tasks +| Property | Description | Type | +| :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | +| `url` | URL to run Cypress against. If running in an environment such as a review or staging app build that has Tool Kit state with a URL for an app to run against, that will override this option. | `string` | -| Task | Description | Default hooks | -|-|-|-| -| `CypressLocal` | Run Cypress against local app | `e2e:local` | -| `CypressCI` | Run Cypress against Heroku apps | `test:review`, `test:staging` | +_All properties are optional._ + diff --git a/plugins/cypress/src/index.ts b/plugins/cypress/src/index.ts index 4c773111c..e69de29bb 100644 --- a/plugins/cypress/src/index.ts +++ b/plugins/cypress/src/index.ts @@ -1,13 +0,0 @@ -import { PackageJsonHook } from '@dotcom-tool-kit/package-json-hook' -import { CypressLocal, CypressCi } from './tasks/cypress' - -class E2ETestHook extends PackageJsonHook { - key = 'e2e-test' - hook = 'e2e:local' -} - -export const hooks = { - 'e2e:local': E2ETestHook -} - -export const tasks = [CypressLocal, CypressCi] diff --git a/plugins/cypress/src/tasks/cypress.ts b/plugins/cypress/src/tasks/cypress.ts index 7e6876145..ec6aaf9d5 100644 --- a/plugins/cypress/src/tasks/cypress.ts +++ b/plugins/cypress/src/tasks/cypress.ts @@ -2,40 +2,35 @@ import { spawn } from 'child_process' import { DopplerEnvVars } from '@dotcom-tool-kit/doppler' import { hookFork, waitOnExit } from '@dotcom-tool-kit/logger' import { readState } from '@dotcom-tool-kit/state' -import { Task } from '@dotcom-tool-kit/types' -import { CypressSchema } from '@dotcom-tool-kit/types/src/schema/cypress' +import { Task } from '@dotcom-tool-kit/base' +import { CypressSchema } from '@dotcom-tool-kit/schemas/lib/tasks/cypress' -export class CypressLocal extends Task { +export default class Cypress extends Task<{ task: typeof CypressSchema }> { async run(): Promise { + const reviewState = readState('review') + const appState = reviewState ?? readState('staging') const cypressEnv: Record = {} - if (this.options.localUrl) { - cypressEnv.CYPRESS_BASE_URL = this.options.localUrl - } + let dopplerEnv = {} - const doppler = new DopplerEnvVars(this.logger, 'dev') - const dopplerEnv = await doppler.get() + if (this.options.url) { + cypressEnv.CYPRESS_BASE_URL = this.options.url + } - const env = { ...process.env, ...dopplerEnv, ...cypressEnv } - this.logger.info('running cypress' + (env.CYPRESS_BASE_URL ? ` against ${env.CYPRESS_BASE_URL}` : '')) - const testProcess = spawn('cypress', ['run'], { env }) - hookFork(this.logger, 'cypress', testProcess) - return waitOnExit('cypress', testProcess) - } -} + if (appState) { + cypressEnv.CYPRESS_BASE_URL = appState.url ?? `https://${appState.appName}.herokuapp.com` -export class CypressCi extends Task { - async run(): Promise { - const reviewState = readState('review') - const cypressEnv: Record = {} - if (reviewState && reviewState.appName) { - cypressEnv.CYPRESS_BASE_URL = `https://${reviewState.appName}.herokuapp.com` - cypressEnv.CYPRESS_REVIEW_APP = 'true' + if (reviewState) { + cypressEnv.CYPRESS_REVIEW_APP = 'true' + } } else { - cypressEnv.CYPRESS_BASE_URL = `https://${process.env.CY_CUSTOM_DOMAIN_STAGING}` + const doppler = new DopplerEnvVars(this.logger, 'dev') + dopplerEnv = await doppler.get() } - this.logger.info(`running cypress against ${cypressEnv.CYPRESS_BASE_URL}`) - const testProcess = spawn('cypress', ['run'], { env: { ...process.env, ...cypressEnv } }) + this.logger.info( + 'running cypress' + (cypressEnv.CYPRESS_BASE_URL ? ` against ${cypressEnv.CYPRESS_BASE_URL}` : '') + ) + const testProcess = spawn('cypress', ['run'], { env: { ...process.env, ...dopplerEnv, ...cypressEnv } }) hookFork(this.logger, 'cypress', testProcess) return waitOnExit('cypress', testProcess) } diff --git a/plugins/cypress/tsconfig.json b/plugins/cypress/tsconfig.json index 6b2c4da0a..bc02a1f0b 100644 --- a/plugins/cypress/tsconfig.json +++ b/plugins/cypress/tsconfig.json @@ -6,7 +6,7 @@ }, "references": [ { - "path": "../../lib/types" + "path": "../../lib/base" }, { "path": "../../lib/logger" @@ -16,7 +16,12 @@ }, { "path": "../../lib/doppler" + }, + { + "path": "../../lib/schemas" } ], - "include": ["src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/plugins/eslint/.toolkitrc.yml b/plugins/eslint/.toolkitrc.yml index c6a9d9f2a..1662d032b 100644 --- a/plugins/eslint/.toolkitrc.yml +++ b/plugins/eslint/.toolkitrc.yml @@ -1,4 +1,9 @@ -hooks: +tasks: + Eslint: './lib/tasks/eslint' + +commands: 'test:local': Eslint 'test:ci': Eslint 'test:staged': Eslint + +version: 2 diff --git a/plugins/eslint/jest.config.js b/plugins/eslint/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/eslint/jest.config.js +++ b/plugins/eslint/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/eslint/package.json b/plugins/eslint/package.json index b828c6a2d..ded1f64aa 100644 --- a/plugins/eslint/package.json +++ b/plugins/eslint/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/eslint", - "version": "3.2.0", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,9 +10,9 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "tslib": "^2.3.1" }, "repository": { @@ -23,9 +23,12 @@ "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/eslint", "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "@types/eslint": "^7.2.13", + "@types/temp": "^0.9.4", "eslint": "^8.15.0", + "temp": "^0.9.4", "winston": "^3.5.1" }, "files": [ @@ -36,11 +39,11 @@ "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x", + "dotcom-tool-kit": "4.0.0-beta.5", "eslint": "7.x || 8.x" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/eslint/readme.md b/plugins/eslint/readme.md index ca76654bd..dfb2993f0 100644 --- a/plugins/eslint/readme.md +++ b/plugins/eslint/readme.md @@ -17,23 +17,18 @@ plugins: - '@dotcom-tool-kit/eslint' ``` -## Options + +## Tasks -| Key | Description | Default value | -|-|-|-| -| `files` | The glob patterns for lint target files. This can either be a string or an array of strings | `'**/*.js'` | -| `options` | The [options](https://eslint.org/docs/latest/developer-guide/nodejs-api#-new-eslintoptions) for the ESLint constructor. This allows for additional flexibility as some of these options, such as `errorOnUnmatchedPattern`, are only applicable at this level but not in your eslintrc.* file | n/a | -| `config` | *Deprecated*. Use `options` instead | n/a | +### `Eslint` -Example: -``` -'@dotcom-tool-kit/eslint': - files: server/*.js' - options: - errorOnUnmatchedPattern: false -``` -## Tasks +Runs `eslint` to lint and format target files. +#### Task options + +| Property | Description | Type | Default | +| :----------- | :------------------------------------------------------------------------------------------------------- | :------------------------ | :------------ | +| `configPath` | Path to the [ESLint config file](https://eslint.org/docs/v8.x/use/configure/configuration-files) to use. | `string` | | +| `files` | The glob patterns for lint target files. This can either be a string or an array of strings. | `Array \| string` | `["**/*.js"]` | -| Task | Description | Preconfigured hooks | -|-|-|-| -| `Eslint` | runs `eslint` to lint and format target files | `test:local`, `test:ci`, `test:staged` | \ No newline at end of file +_All properties are optional._ + diff --git a/plugins/eslint/src/index.ts b/plugins/eslint/src/index.ts deleted file mode 100644 index d599ac30e..000000000 --- a/plugins/eslint/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Eslint from './tasks/eslint' - -export const tasks = [Eslint] diff --git a/plugins/eslint/src/tasks/eslint.ts b/plugins/eslint/src/tasks/eslint.ts index b55f0fea6..622a4dd8d 100644 --- a/plugins/eslint/src/tasks/eslint.ts +++ b/plugins/eslint/src/tasks/eslint.ts @@ -1,14 +1,12 @@ import { ToolKitError } from '@dotcom-tool-kit/error' import { styles } from '@dotcom-tool-kit/logger' -import { Task } from '@dotcom-tool-kit/types' -import { ESLintSchema } from '@dotcom-tool-kit/types/lib/schema/eslint' +import { Task, TaskRunContext } from '@dotcom-tool-kit/base' +import { ESLintSchema } from '@dotcom-tool-kit/schemas/lib/tasks/eslint' import { ESLint } from 'eslint' -export default class Eslint extends Task { - static description = '' - - async run(files?: string[]): Promise { - const eslint = new ESLint(this.options.options) +export default class Eslint extends Task<{ task: typeof ESLintSchema }> { + async run({ files }: TaskRunContext): Promise { + const eslint = new ESLint({ overrideConfigFile: this.options.configPath }) const results = await eslint.lintFiles(files ?? this.options.files) const formatter = await eslint.loadFormatter('stylish') const resultText = formatter.format(results) diff --git a/plugins/eslint/test/files/.eslintrc.js b/plugins/eslint/test/files/.eslintrc.js deleted file mode 100644 index b44fe5bce..000000000 --- a/plugins/eslint/test/files/.eslintrc.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - env: { - commonjs: true, - es2021: true, - node: true - }, - extends: 'eslint:recommended', - overrides: [], - parserOptions: { - ecmaVersion: 'latest' - }, - rules: {}, - root: true -} diff --git a/plugins/eslint/test/files/fail.js b/plugins/eslint/test/files/fail.js deleted file mode 100644 index 7211b2c8a..000000000 --- a/plugins/eslint/test/files/fail.js +++ /dev/null @@ -1,4 +0,0 @@ -function lintFail() { - let test = 'hello?' - console.log(test) -} diff --git a/plugins/eslint/test/files/pass.js b/plugins/eslint/test/files/pass.js deleted file mode 100644 index 8d2f0971e..000000000 --- a/plugins/eslint/test/files/pass.js +++ /dev/null @@ -1 +0,0 @@ -1 + 1 diff --git a/plugins/eslint/test/tasks/eslint.test.ts b/plugins/eslint/test/tasks/eslint.test.ts index d0e412284..96294184e 100644 --- a/plugins/eslint/test/tasks/eslint.test.ts +++ b/plugins/eslint/test/tasks/eslint.test.ts @@ -1,34 +1,64 @@ -import { ToolKitError } from '@dotcom-tool-kit/error/lib' import { describe, it, expect } from '@jest/globals' import * as path from 'path' import winston, { Logger } from 'winston' import ESLint from '../../src/tasks/eslint' +import temp from 'temp' +import fs from 'fs/promises' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger -const testDirectory = path.join(__dirname, '../files') +temp.track() describe('eslint', () => { + let testDirectory: string + + beforeAll(async () => { + testDirectory = await temp.mkdir('eslint') + + await fs.writeFile( + path.join(testDirectory, '.eslintrc.js'), + `module.exports = { + extends: 'eslint:recommended', + root: true + }` + ) + + await fs.writeFile(path.join(testDirectory, 'pass.js'), `1 + 1`) + await fs.writeFile(path.join(testDirectory, 'fail.js'), `undeclared`) + }) + + afterAll(async () => { + await temp.cleanup() + }) + it('should pass on correct file', async () => { - const task = new ESLint(logger, { - options: { ignore: false, cwd: testDirectory }, - files: [path.join(testDirectory, 'pass.js')] - }) + const task = new ESLint( + logger, + 'ESLint', + {}, + { + configPath: path.join(testDirectory, '.eslintrc.js'), + files: [path.join(testDirectory, 'pass.js')] + } + ) - await task.run() + await expect(task.run({ command: 'test:local' })).resolves.toBeUndefined() }) it('should fail on linter error', async () => { - const task = new ESLint(logger, { - options: { ignore: false, cwd: testDirectory }, - files: [path.join(testDirectory, 'fail.js')] - }) - - expect.assertions(1) - try { - await task.run() - } catch (err) { - if (err instanceof ToolKitError) expect(err.details).toContain('1 problem (1 error, 0 warnings)') - } + const task = new ESLint( + logger, + 'ESLint', + {}, + { + configPath: path.join(testDirectory, '.eslintrc.js'), + files: [path.join(testDirectory, 'fail.js')] + } + ) + + await expect(task.run({ command: 'test:local' })).rejects.toHaveProperty( + 'details', + expect.stringContaining('1 problem (1 error, 0 warnings)') + ) }) }) diff --git a/plugins/eslint/tsconfig.json b/plugins/eslint/tsconfig.json index 3f2a5796a..459bea939 100644 --- a/plugins/eslint/tsconfig.json +++ b/plugins/eslint/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.settings.json", - "include": ["src/**/*"], + "include": [ + "src/**/*" + ], "compilerOptions": { "outDir": "lib", "rootDir": "src" @@ -13,7 +15,10 @@ "path": "../../lib/logger" }, { - "path": "../../lib/types" + "path": "../../lib/base" + }, + { + "path": "../../lib/schemas" } ] } diff --git a/plugins/frontend-app/.toolkitrc.yml b/plugins/frontend-app/.toolkitrc.yml index d5f03d7e2..d5e4ccbdd 100644 --- a/plugins/frontend-app/.toolkitrc.yml +++ b/plugins/frontend-app/.toolkitrc.yml @@ -3,8 +3,10 @@ plugins: - '@dotcom-tool-kit/backend-heroku-app' - '@dotcom-tool-kit/upload-assets-to-s3' -hooks: +commands: 'run:local': - WebpackDevelopment # run a webpack compile before starting the server because dotcom-server-asset-loader expects a manifest to exist - Node - WebpackWatch + +version: 2 diff --git a/plugins/frontend-app/index.js b/plugins/frontend-app/index.js index c32ed7534..e69de29bb 100644 --- a/plugins/frontend-app/index.js +++ b/plugins/frontend-app/index.js @@ -1 +0,0 @@ -exports.tasks = [] diff --git a/plugins/frontend-app/package.json b/plugins/frontend-app/package.json index 5908b36e9..270eaf572 100644 --- a/plugins/frontend-app/package.json +++ b/plugins/frontend-app/package.json @@ -1,15 +1,15 @@ { "name": "@dotcom-tool-kit/frontend-app", - "version": "3.2.4", + "version": "4.0.0-beta.6", "description": "", "main": "index.js", "keywords": [], "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/backend-heroku-app": "^3.1.4", - "@dotcom-tool-kit/upload-assets-to-s3": "^3.2.0", - "@dotcom-tool-kit/webpack": "^3.2.0" + "@dotcom-tool-kit/backend-heroku-app": "4.0.0-beta.6", + "@dotcom-tool-kit/upload-assets-to-s3": "4.0.0-beta.5", + "@dotcom-tool-kit/webpack": "4.0.0-beta.5" }, "repository": { "type": "git", @@ -22,10 +22,10 @@ "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/heroku/.toolkitrc.yml b/plugins/heroku/.toolkitrc.yml index 6fd954f47..704143253 100644 --- a/plugins/heroku/.toolkitrc.yml +++ b/plugins/heroku/.toolkitrc.yml @@ -1,6 +1,24 @@ plugins: + - '@dotcom-tool-kit/package-json-hook' - '@dotcom-tool-kit/npm' - '@dotcom-tool-kit/doppler' # required so the create script knows we need its options -hooks: +tasks: + HerokuProduction: './lib/tasks/production' + HerokuStaging: './lib/tasks/staging' + HerokuReview: './lib/tasks/review' + HerokuTeardown: './lib/tasks/teardown' + +commands: 'cleanup:remote': NpmPrune + +options: + hooks: + - PackageJson: + scripts: + heroku-postbuild: + - 'build:remote' + - 'release:remote' + - 'cleanup:remote' + +version: 2 diff --git a/plugins/heroku/jest.config.js b/plugins/heroku/jest.config.js index e427ce8cb..2ea38bb31 100644 --- a/plugins/heroku/jest.config.js +++ b/plugins/heroku/jest.config.js @@ -1,6 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base, - collectCoverage: true + ...base.config } diff --git a/plugins/heroku/package.json b/plugins/heroku/package.json index 772fb7737..95e21860b 100644 --- a/plugins/heroku/package.json +++ b/plugins/heroku/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/heroku", - "version": "3.4.1", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,15 +10,15 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/doppler": "^1.1.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/npm": "^3.3.1", - "@dotcom-tool-kit/options": "^3.2.0", - "@dotcom-tool-kit/package-json-hook": "^4.2.0", - "@dotcom-tool-kit/state": "^3.3.0", - "@dotcom-tool-kit/types": "^3.6.0", - "@dotcom-tool-kit/wait-for-ok": "^3.2.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/npm": "4.0.0-beta.5", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", + "@dotcom-tool-kit/state": "4.0.0-beta.0", + "@dotcom-tool-kit/wait-for-ok": "4.0.0-beta.0", "@octokit/request": "^5.6.0", "@octokit/request-error": "^2.1.0", "heroku-client": "^3.1.0", @@ -34,7 +34,8 @@ "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/heroku", "devDependencies": { - "@types/financial-times__package-json": "^1.9.0", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", + "@types/financial-times__package-json": "2.0.0-beta.0", "@types/p-retry": "^3.0.1", "winston": "^3.5.1" }, @@ -46,10 +47,10 @@ "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/heroku/readme.md b/plugins/heroku/readme.md index 89bd87061..9dbac5ec2 100644 --- a/plugins/heroku/readme.md +++ b/plugins/heroku/readme.md @@ -19,48 +19,61 @@ plugins: - '@dotcom-tool-kit/heroku' ``` -And install this plugin's hooks: + +## Tasks -```sh -npx dotcom-tool-kit --install -``` +### `HerokuProduction` + +Promote the Heroku staging app to production. + +#### Task options -This plugin cannot currently automatically install itself to heroku configuration, so it will exit, and explain what you need to include in the config. +`scaling`: an object with scaling configuration for each app and dyno. The first-level keys are the names of your production apps, and the second level keys are names of the dynos within each app (this should usually at least include `web`). -## Options +##### Scaling configuration -| Key | Description | Default value | +| Property | Description | Type | |-|-|-| -| `pipeline` | (required) the pipeline name for your application as it's defined in Heroku | none | -| `systemCode` | (required) system code for your application as it's defined in Biz Ops | none | -| `scaling` | (required) configuration for dyno scaling with a quantity and size for each production app | none | +| `size` | the [Dyno type](https://devcenter.heroku.com/articles/dyno-types) for this dyno, e.g. `standard-1x`. apps in the FT Heroku account can only use [professional tier dynos](https://devcenter.heroku.com/articles/dyno-types#dyno-tiers-and-mixing-dyno-types). | `string` | +| `quantity` | how many of this dyno to use | `number` | -Here's what your configuration might look like passing in the above options: +##### Example -```yml +~~~yml options: - '@dotcom-tool-kit/heroku': - pipeline: 'ft-next-static' - systemCode: 'next-static' - scaling: - ft-next-static-eu: - web: - size: standard-1x - quantity: 1 -``` + tasks: + HerokuProduction: + scaling: + ft-next-static-eu: + web: + size: standard-1x + quantity: 1 +~~~ -## Hooks + -| Event | Description | Installed to... | -|-|-|-| -| `build:remote` | Compile any assets or code required for your app to run. | `heroku-postbuild` script in `package.json` | -| `release:remote` | Run any post-release tasks for an app that require a tool-kit task, e.g. upload assets to s3 | `heroku-postbuild` script in `package.json` | -## Tasks +### `HerokuStaging` -| Task | Description | Preconfigured hooks | -|-|-|-| -| `HerokuProduction` | deploy production app to Heroku via CircleCi | `deploy:production` | -| `HerokuStaging` | deploy to staging | `deploy:staging` | -| `HerokuReview` | create and test Heroku review app | `deploy:review` | -| `HerokuTeardown` | scale down staging once smoke tests have been run | `teardown:staging` | +Deploy to the Heroku staging app. + +### `HerokuReview` + +Create and deploy a Heroku review app. + +### `HerokuTeardown` + +Scale down the Heroku staging app once it's no longer needed. + + +## Plugin-wide options + +### `@dotcom-tool-kit/heroku` + +| Property | Description | Type | +| :-------------------- | :-------------------------------------------------------------------------------------------------------------- | :------- | +| **`pipeline`** (\*) | the ID of your app's Heroku pipeline. this can be found at https://dashboard.heroku.com/pipelines/[PIPELINE_ID] | `string` | +| **`systemCode`** (\*) | your app's Biz Ops system code. this can be found at https://biz-ops.in.ft.com/System/[SYSTEM_CODE] | `string` | + +_(\*) Required._ + diff --git a/plugins/heroku/src/declarations.d.ts b/plugins/heroku/src/declarations.d.ts index 034f9a0ec..0eab8150d 100644 --- a/plugins/heroku/src/declarations.d.ts +++ b/plugins/heroku/src/declarations.d.ts @@ -30,7 +30,7 @@ declare module 'heroku-client' { export type HerokuApiResGetApp = { id: string - slugSize: number | null + slug_size: number | null } export type HerokuApiResGetReview = { diff --git a/plugins/heroku/src/index.ts b/plugins/heroku/src/index.ts deleted file mode 100644 index 6cd498d7b..000000000 --- a/plugins/heroku/src/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import HerokuProduction from './tasks/production' -import HerokuStaging from './tasks/staging' -import HerokuReview from './tasks/review' -import HerokuTeardown from './tasks/teardown' -import { PackageJsonScriptHook } from '@dotcom-tool-kit/package-json-hook' - -class BuildRemote extends PackageJsonScriptHook { - key = 'heroku-postbuild' - hook = 'build:remote' -} - -class CleanupRemote extends PackageJsonScriptHook { - key = 'heroku-postbuild' - hook = 'cleanup:remote' -} - -class ReleaseRemote extends PackageJsonScriptHook { - key = 'heroku-postbuild' - hook = 'release:remote' -} - -export const hooks = { - 'cleanup:remote': CleanupRemote, - 'release:remote': ReleaseRemote, - 'build:remote': BuildRemote -} - -export const tasks = [HerokuProduction, HerokuStaging, HerokuReview, HerokuTeardown] diff --git a/plugins/heroku/src/tasks/production.ts b/plugins/heroku/src/tasks/production.ts index 5b2416466..a265de8c1 100644 --- a/plugins/heroku/src/tasks/production.ts +++ b/plugins/heroku/src/tasks/production.ts @@ -1,16 +1,18 @@ -import { Task } from '@dotcom-tool-kit/types' +import { Task } from '@dotcom-tool-kit/base' import { ToolKitError } from '@dotcom-tool-kit/error' import { readState } from '@dotcom-tool-kit/state' import { styles } from '@dotcom-tool-kit/logger' -import { HerokuSchema } from '@dotcom-tool-kit/types/lib/schema/heroku' +import { HerokuSchema } from '@dotcom-tool-kit/schemas/lib/plugins/heroku' import type { HerokuApiResGetApp } from 'heroku-client' import heroku, { extractHerokuError } from '../herokuClient' import { scaleDyno } from '../scaleDyno' import { promoteStagingToProduction } from '../promoteStagingToProduction' +import { HerokuProductionSchema } from '@dotcom-tool-kit/schemas/src/tasks/heroku-production' -export default class HerokuProduction extends Task { - static description = '' - +export default class HerokuProduction extends Task<{ + plugin: typeof HerokuSchema + task: typeof HerokuProductionSchema +}> { async run(): Promise { try { this.logger.verbose('retrieving staging slug...') @@ -33,7 +35,7 @@ export default class HerokuProduction extends Task { } const promote = async () => { this.logger.verbose('promoting staging to production....') - await promoteStagingToProduction(this.logger, slugId, this.options.systemCode) + await promoteStagingToProduction(this.logger, slugId, this.pluginOptions.systemCode) this.logger.info('staging has been successfully promoted to production') } @@ -76,6 +78,6 @@ export default class HerokuProduction extends Task { const appInfo = await heroku .get(`/apps/${appId}`) .catch(extractHerokuError(`getting slug size for app ${appId}`)) - return appInfo.slugSize !== null + return appInfo.slug_size !== null } } diff --git a/plugins/heroku/src/tasks/review.ts b/plugins/heroku/src/tasks/review.ts index b3b529bf1..faa4127dc 100644 --- a/plugins/heroku/src/tasks/review.ts +++ b/plugins/heroku/src/tasks/review.ts @@ -1,22 +1,20 @@ -import { Task } from '@dotcom-tool-kit/types' +import { Task } from '@dotcom-tool-kit/base' import { getHerokuReviewApp } from '../getHerokuReviewApp' import { buildHerokuReviewApp } from '../buildHerokuReviewApp' import { gtg } from '../gtg' import { setStageConfigVars } from '../setConfigVars' import { writeState } from '@dotcom-tool-kit/state' -import { HerokuSchema } from '@dotcom-tool-kit/types/lib/schema/heroku' +import { HerokuSchema } from '@dotcom-tool-kit/schemas/lib/plugins/heroku' import { ToolKitError } from '@dotcom-tool-kit/error' import herokuClient, { extractHerokuError } from '../herokuClient' import type { HerokuApiResPipeline } from 'heroku-client' -export default class HerokuReview extends Task { - static description = '' - +export default class HerokuReview extends Task<{ plugin: typeof HerokuSchema }> { async run(): Promise { try { const pipeline = await herokuClient - .get(`/pipelines/${this.options.pipeline}`) - .catch(extractHerokuError(`getting pipeline ${this.options.pipeline}`)) + .get(`/pipelines/${this.pluginOptions.pipeline}`) + .catch(extractHerokuError(`getting pipeline ${this.pluginOptions.pipeline}`)) await setStageConfigVars(this.logger, 'review', 'prod', pipeline.id) let reviewAppId = await getHerokuReviewApp(this.logger, pipeline.id) diff --git a/plugins/heroku/src/tasks/staging.ts b/plugins/heroku/src/tasks/staging.ts index 1653077a9..77b6c026c 100644 --- a/plugins/heroku/src/tasks/staging.ts +++ b/plugins/heroku/src/tasks/staging.ts @@ -1,4 +1,4 @@ -import { Task } from '@dotcom-tool-kit/types' +import { Task } from '@dotcom-tool-kit/base' import { ToolKitError } from '@dotcom-tool-kit/error' import { getHerokuStagingApp } from '../getHerokuStagingApp' import { setAppConfigVars } from '../setConfigVars' @@ -7,22 +7,20 @@ import { repeatedCheckForBuildSuccess } from '../repeatedCheckForBuildSuccess' import { scaleDyno } from '../scaleDyno' import { gtg } from '../gtg' import { getPipelineCouplings } from '../getPipelineCouplings' -import { HerokuSchema } from '@dotcom-tool-kit/types/lib/schema/heroku' +import { HerokuSchema } from '@dotcom-tool-kit/schemas/lib/plugins/heroku' import { setStagingSlug } from '../setStagingSlug' -export default class HerokuStaging extends Task { - static description = '' - +export default class HerokuStaging extends Task<{ plugin: typeof HerokuSchema }> { async run(): Promise { try { this.logger.verbose(`retrieving pipeline details...`) - await getPipelineCouplings(this.logger, this.options.pipeline) + await getPipelineCouplings(this.logger, this.pluginOptions.pipeline) this.logger.verbose(`retrieving staging app details...`) const appName = await getHerokuStagingApp() // setting config vars on staging from the doppler production directory - await setAppConfigVars(this.logger, appName, 'prod', this.options.systemCode) + await setAppConfigVars(this.logger, appName, 'prod', this.pluginOptions.systemCode) // create build from latest commit, even on no change const buildDetails = await createBuild(this.logger, appName) diff --git a/plugins/heroku/src/tasks/teardown.ts b/plugins/heroku/src/tasks/teardown.ts index 92e7ae836..8e6920c18 100644 --- a/plugins/heroku/src/tasks/teardown.ts +++ b/plugins/heroku/src/tasks/teardown.ts @@ -1,12 +1,10 @@ -import { Task } from '@dotcom-tool-kit/types' +import { Task } from '@dotcom-tool-kit/base' import { readState } from '@dotcom-tool-kit/state' import { styles } from '@dotcom-tool-kit/logger' import { scaleDyno } from '../scaleDyno' import { ToolKitError } from '@dotcom-tool-kit/error' export default class HerokuTeardown extends Task { - static description = '' - async run(): Promise { //scale down staging const state = readState('staging') diff --git a/plugins/heroku/test/index.test.ts b/plugins/heroku/test/index.test.ts deleted file mode 100644 index cf315f70d..000000000 --- a/plugins/heroku/test/index.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { describe, it, expect } from '@jest/globals' -import * as heroku from '../' - -describe('Heroku plugin', () => { - it('should define Heroku build hooks', () => { - expect(heroku.hooks).toEqual( - expect.objectContaining({ - 'build:remote': expect.any(Function), - 'release:remote': expect.any(Function) - }) - ) - }) -}) diff --git a/plugins/heroku/test/setConfigVars.test.ts b/plugins/heroku/test/setConfigVars.test.ts index 1287cb4e6..d5b708d97 100644 --- a/plugins/heroku/test/setConfigVars.test.ts +++ b/plugins/heroku/test/setConfigVars.test.ts @@ -4,7 +4,6 @@ import { DopplerEnvVars } from '@dotcom-tool-kit/doppler' import heroku from '../src/herokuClient' import winston, { Logger } from 'winston' const logger = (winston as unknown) as Logger -/* eslint-disable @typescript-eslint/no-unused-vars */ type DopplerPath = { project: string } @@ -38,8 +37,6 @@ const reviewPatchBody = { } class DopplerEnvVarsMock { dopplerPath: DopplerPath - // Intentional unused parameter as pre-fixed with an underscore - // eslint-disable-next-line no-unused-vars constructor(_dopplerPath: DopplerPath, public environment: string, private migrated: boolean) { this.dopplerPath = dopplerPath } diff --git a/plugins/heroku/test/tasks/production.test.ts b/plugins/heroku/test/tasks/production.test.ts index 91345b45f..3e0d1a987 100644 --- a/plugins/heroku/test/tasks/production.test.ts +++ b/plugins/heroku/test/tasks/production.test.ts @@ -3,7 +3,7 @@ import Production from '../../src/tasks/production' import * as utils from '../../src/promoteStagingToProduction' import winston, { Logger } from 'winston' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger jest.mock('@dotcom-tool-kit/state', () => { return { @@ -15,28 +15,32 @@ jest.mock('../../src/scaleDyno') const mockPromoteStagingToProduction = jest.spyOn(utils, 'promoteStagingToProduction') jest.spyOn(Production.prototype, 'fetchIfAppHasDeployed').mockImplementation(() => Promise.resolve(true)) +const pluginOptions = { + pipeline: 'next-health', + systemCode: 'next-health' +} + const productionOptions = { - systemCode: 'next-health', scaling: { 'test-app': { web: { size: 'standard-1x', quantity: 1 } } } } describe('staging', () => { it('should call set slug with slug id and system code', async () => { mockPromoteStagingToProduction.mockImplementation(() => Promise.resolve([])) - const task = new Production(logger, productionOptions) + const task = new Production(logger, 'HerokuProduction', pluginOptions, productionOptions) await task.run() expect(utils.promoteStagingToProduction).toBeCalledWith(expect.anything(), 'slug-id', 'next-health') }) it('should resolve when completed successfully', async () => { - const task = new Production(logger, productionOptions) + const task = new Production(logger, 'HerokuProduction', pluginOptions, productionOptions) await expect(task.run()).resolves.not.toThrow() }) it('should throw if it completes unsuccessfully', async () => { mockPromoteStagingToProduction.mockImplementation(() => Promise.reject()) - const task = new Production(logger, productionOptions) + const task = new Production(logger, 'HerokuProduction', pluginOptions, productionOptions) await expect(task.run()).rejects.toThrowError() }) }) diff --git a/plugins/heroku/test/tasks/review.test.ts b/plugins/heroku/test/tasks/review.test.ts index 2cec3d68a..26e4e0b7f 100644 --- a/plugins/heroku/test/tasks/review.test.ts +++ b/plugins/heroku/test/tasks/review.test.ts @@ -59,18 +59,8 @@ jest.mock('../../src/gtg', () => { }) describe('review', () => { - it('should fail when pipeline option is missing', async () => { - const task = new Review(logger, {}) - - try { - await task.run() - } catch (err) { - expect(err).toBeTruthy() - } - }) - it('should call pass in the pipeline id to heroku api call', async () => { - const task = new Review(logger, { pipeline }) + const task = new Review(logger, 'HerokuReview', { pipeline, systemCode: '', scaling: {} }) await task.run() @@ -79,7 +69,7 @@ describe('review', () => { }) it('should return review app id from get heroku review app', async () => { - const task = new Review(logger, { pipeline }) + const task = new Review(logger, 'HerokuReview', { pipeline, systemCode: '', scaling: {} }) await task.run() @@ -88,7 +78,7 @@ describe('review', () => { }) it('should fail if either doppler option is missing', async () => { - let task = new Review(logger, { pipeline }) + let task = new Review(logger, 'HerokuReview', { pipeline, systemCode: '', scaling: {} }) try { await task.run() @@ -96,7 +86,7 @@ describe('review', () => { expect(err).toBeTruthy() } - task = new Review(logger, { pipeline }) + task = new Review(logger, 'HerokuReview', { pipeline, systemCode: '', scaling: {} }) try { await task.run() @@ -106,7 +96,7 @@ describe('review', () => { }) it('should call setStageConfigVars with doppler project', async () => { - const task = new Review(logger, { pipeline }) + const task = new Review(logger, 'HerokuReview', { pipeline, systemCode: '', scaling: {} }) await task.run() @@ -114,7 +104,7 @@ describe('review', () => { }) it('should write app id to state', async () => { - const task = new Review(logger, { pipeline }) + const task = new Review(logger, 'HerokuReview', { pipeline, systemCode: '', scaling: {} }) await task.run() @@ -122,7 +112,7 @@ describe('review', () => { }) it('should call gtg with appName', async () => { - const task = new Review(logger, { pipeline }) + const task = new Review(logger, 'HerokuReview', { pipeline, systemCode: '', scaling: {} }) await task.run() @@ -131,7 +121,7 @@ describe('review', () => { it('should throw an error if it fails', async () => { pipeline = 'wrong-pipeline-name' - const task = new Review(logger, { pipeline }) + const task = new Review(logger, 'HerokuReview', { pipeline, systemCode: '', scaling: {} }) try { await task.run() diff --git a/plugins/heroku/test/tasks/staging.test.ts b/plugins/heroku/test/tasks/staging.test.ts index bddf410ae..6fdad4479 100644 --- a/plugins/heroku/test/tasks/staging.test.ts +++ b/plugins/heroku/test/tasks/staging.test.ts @@ -75,36 +75,8 @@ describe('staging', () => { buildInfo.slug = null }) - it('should fail when both options are missing', async () => { - const task = new Staging(logger, {}) - - try { - await task.run() - } catch (err) { - expect(err).toBeTruthy() - } - }) - - it('should fail if either option is missing', async () => { - let task = new Staging(logger, { pipeline }) - - try { - await task.run() - } catch (err) { - expect(err).toBeTruthy() - } - - task = new Staging(logger, { pipeline, systemCode }) - - try { - await task.run() - } catch (err) { - expect(err).toBeTruthy() - } - }) - it('should call pipeline couplings with pipeline option', async () => { - const task = new Staging(logger, { pipeline, systemCode }) + const task = new Staging(logger, 'HerokuStaging', { pipeline, systemCode, scaling: {} }) await task.run() @@ -113,7 +85,7 @@ describe('staging', () => { }) it('should return appName from get heroku staging', async () => { - const task = new Staging(logger, { pipeline, systemCode }) + const task = new Staging(logger, 'HerokuStaging', { pipeline, systemCode, scaling: {} }) await task.run() @@ -122,7 +94,7 @@ describe('staging', () => { }) it('should call setAppConfigVars with doppler project and system code', async () => { - const task = new Staging(logger, { pipeline, systemCode }) + const task = new Staging(logger, 'HerokuStaging', { pipeline, systemCode, scaling: {} }) await task.run() @@ -130,7 +102,7 @@ describe('staging', () => { }) it('should call createBuild with app name', async () => { - const task = new Staging(logger, { pipeline, systemCode }) + const task = new Staging(logger, 'HerokuStaging', { pipeline, systemCode, scaling: {} }) await task.run() @@ -138,21 +110,21 @@ describe('staging', () => { }) it(`should call repeatedCheckForBuildSuccess if the slug id isn't present`, async () => { - const task = new Staging(logger, { pipeline, systemCode }) + const task = new Staging(logger, 'HerokuStaging', { pipeline, systemCode, scaling: {} }) await task.run() expect(repeatedCheckForBuildSuccess).toBeCalledWith(expect.anything(), appName, buildInfo.id) }) it('should call setStagingSlug with app name and slug id', async () => { - const task = new Staging(logger, { pipeline, systemCode }) + const task = new Staging(logger, 'HerokuStaging', { pipeline, systemCode, scaling: {} }) await task.run() expect(setStagingSlug).toBeCalledWith(expect.anything(), appName, slugId) }) it('should call scaleDyno', async () => { - const task = new Staging(logger, { pipeline, systemCode }) + const task = new Staging(logger, 'HerokuStaging', { pipeline, systemCode, scaling: {} }) await task.run() @@ -160,25 +132,15 @@ describe('staging', () => { }) it('should call gtg with appName', async () => { - const task = new Staging(logger, { pipeline, systemCode }) + const task = new Staging(logger, 'HerokuStaging', { pipeline, systemCode, scaling: {} }) await task.run() expect(gtg).toBeCalledWith(expect.anything(), appName, 'staging') }) - it('should throw an error if it fails', async () => { - const task = new Staging(logger, { pipeline }) - - try { - await task.run() - } catch (err) { - expect(err).toBeTruthy() - } - }) - it('should resolve successfully when complete', async () => { - const task = new Staging(logger, { pipeline, systemCode }) + const task = new Staging(logger, 'HerokuStaging', { pipeline, systemCode, scaling: {} }) await expect(task.run()).resolves.not.toThrow() }) diff --git a/plugins/heroku/test/tasks/teardown.test.ts b/plugins/heroku/test/tasks/teardown.test.ts index f4e7d2d5d..3946aa9ab 100644 --- a/plugins/heroku/test/tasks/teardown.test.ts +++ b/plugins/heroku/test/tasks/teardown.test.ts @@ -18,7 +18,7 @@ const mockScaleDyno = jest.spyOn(utils, 'scaleDyno') describe('teardown', () => { it('should call scaleDyno with app name', async () => { mockScaleDyno.mockImplementationOnce(() => Promise.resolve()) - const task = new Teardown(logger, {}) + const task = new Teardown(logger, 'HerokuTeardown', {}) await task.run() expect(utils.scaleDyno).toHaveBeenCalledWith(expect.anything(), appName, 0) @@ -26,13 +26,13 @@ describe('teardown', () => { it('should resolve if succesfully completed', async () => { mockScaleDyno.mockImplementationOnce(() => Promise.resolve()) - const task = new Teardown(logger, {}) + const task = new Teardown(logger, 'HerokuTeardown', {}) await expect(task.run()).resolves.not.toThrow() }) it('should return a rejected promise if it completes unsuccessfully', async () => { mockScaleDyno.mockImplementationOnce(() => Promise.reject()) - const task = new Teardown(logger, {}) + const task = new Teardown(logger, 'HerokuTeardown', {}) await expect(task.run()).rejects.toThrowError() }) }) diff --git a/plugins/heroku/tsconfig.json b/plugins/heroku/tsconfig.json index 36c8ad192..0a6805af7 100644 --- a/plugins/heroku/tsconfig.json +++ b/plugins/heroku/tsconfig.json @@ -14,24 +14,29 @@ "path": "../../lib/doppler" }, { - "path": "../../lib/package-json-hook" + "path": "../../plugins/package-json-hook" }, { "path": "../npm" }, { - "path": "../../lib/types" + "path": "../../lib/base" }, { "path": "../../lib/logger" }, { "path": "../../lib/options" + }, + { + "path": "../../lib/schemas" } ], "compilerOptions": { "outDir": "lib", "rootDir": "src" }, - "include": ["src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/plugins/husky-npm/.toolkitrc.yml b/plugins/husky-npm/.toolkitrc.yml index e69de29bb..c679cbcea 100644 --- a/plugins/husky-npm/.toolkitrc.yml +++ b/plugins/husky-npm/.toolkitrc.yml @@ -0,0 +1,11 @@ +plugins: + - '@dotcom-tool-kit/package-json-hook' + +options: + hooks: + - PackageJson: + husky.hooks: + pre-commit: 'git:precommit' + commit-msg: 'git:commitmsg' + +version: 2 diff --git a/plugins/husky-npm/jest.config.js b/plugins/husky-npm/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/husky-npm/jest.config.js +++ b/plugins/husky-npm/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/husky-npm/package.json b/plugins/husky-npm/package.json index 6d7e3da27..608dc86e6 100644 --- a/plugins/husky-npm/package.json +++ b/plugins/husky-npm/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/husky-npm", - "version": "4.2.0", + "version": "5.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,12 +10,12 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/package-json-hook": "^4.2.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", "tslib": "^2.3.1" }, "peerDependencies": { - "husky": "4.x", - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5", + "husky": "4.x" }, "repository": { "type": "git", @@ -32,7 +32,7 @@ "extends": "../../package.json" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/husky-npm/src/husky-hook.ts b/plugins/husky-npm/src/husky-hook.ts deleted file mode 100644 index 50adcfc15..000000000 --- a/plugins/husky-npm/src/husky-hook.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { PackageJsonHelper } from '@dotcom-tool-kit/package-json-hook' - -export abstract class HuskyHook extends PackageJsonHelper { - field = ['husky', 'hooks'] -} diff --git a/plugins/husky-npm/src/index.ts b/plugins/husky-npm/src/index.ts index 96ae851a8..e69de29bb 100644 --- a/plugins/husky-npm/src/index.ts +++ b/plugins/husky-npm/src/index.ts @@ -1,18 +0,0 @@ -import { HuskyHook } from './husky-hook' - -class GitPrecommit extends HuskyHook { - key = 'pre-commit' - hook = 'git:precommit' -} - -class GitCommitmsg extends HuskyHook { - key = 'commit-msg' - hook = 'git:commitmsg' -} - -export { HuskyHook } - -export const hooks = { - 'git:precommit': GitPrecommit, - 'git:commitmsg': GitCommitmsg -} diff --git a/plugins/husky-npm/tsconfig.json b/plugins/husky-npm/tsconfig.json index 8470223fa..0939ce186 100644 --- a/plugins/husky-npm/tsconfig.json +++ b/plugins/husky-npm/tsconfig.json @@ -6,10 +6,8 @@ }, "references": [ { - "path": "../../lib/package-json-hook" + "path": "../../plugins/package-json-hook" } ], - "include": [ - "src/**/*" - ] + "include": ["src/**/*"] } diff --git a/plugins/jest/.toolkitrc.yml b/plugins/jest/.toolkitrc.yml index c87be6023..6ae2716b0 100644 --- a/plugins/jest/.toolkitrc.yml +++ b/plugins/jest/.toolkitrc.yml @@ -1,3 +1,8 @@ -hooks: - 'test:local': JestLocal - 'test:ci': JestCI +tasks: + Jest: './lib/tasks/jest' + +commands: + 'test:local': Jest + 'test:ci': Jest # TODO provide default mode option here + +version: 2 diff --git a/plugins/jest/jest.config.js b/plugins/jest/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/jest/jest.config.js +++ b/plugins/jest/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/jest/package.json b/plugins/jest/package.json index beb0ba7c2..2785d75f1 100644 --- a/plugins/jest/package.json +++ b/plugins/jest/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/jest", - "version": "3.4.0", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,13 +10,13 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "tslib": "^2.3.1" }, "peerDependencies": { - "jest-cli": "27.x || 28.x || 29.x", - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5", + "jest-cli": "27.x || 28.x || 29.x" }, "repository": { "type": "git", @@ -30,11 +30,11 @@ ".toolkitrc.yml" ], "devDependencies": { - "@jest/globals": "^27.4.6", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "winston": "^3.5.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/jest/readme.md b/plugins/jest/readme.md index 796032f75..e4e6391d1 100644 --- a/plugins/jest/readme.md +++ b/plugins/jest/readme.md @@ -17,29 +17,33 @@ plugins: - '@dotcom-tool-kit/jest' ``` -## Options + +## Tasks -| Key | Description | Default value | -|-|-|-| -| `configPath` | Path to the [Jest config file](https://jestjs.io/docs/27.x/configuration) | use Jest's own [config resolution](https://jestjs.io/docs/configuration/) | +### `Jest` -## Tasks +Runs `jest` to execute tests. +#### Task options -| Task | Description | Preconfigured hook | -|-|-|-| -| `JestLocal` | runs `jest` to execute tests | `test:local` | -| `JestCI` | runs `jest` to execute tests in the CI with the `--ci` [option](https://jestjs.io/docs/cli#--ci) | `test:ci` | +| Property | Description | Type | +| :----------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | +| `configPath` | Path to the [Jest config file](https://jestjs.io/docs/27.x/configuration). Use Jest's own [config resolution](https://jestjs.io/docs/configuration/) by default. | `string` | +| `ci` | Whether to run Jest in [CI mode](https://jestjs.io/docs/cli#--ci). | `true` | + +_All properties are optional._ + ## Tips -A common use case is to configure `test:local` and `test:ci` in your `.toolkitrc.yml` to run the `Eslint` task then the relevant Jest task: +A common use case is to configure `test:local` and `test:ci` in your `.toolkitrc.yml` to run the `Eslint` task then the relevant Jest task: ```yaml -hooks: +commands: test:local: - Eslint - - JestLocal + - Jest test:ci: - Eslint - - JestCI -``` \ No newline at end of file + - Jest: + ci: true +``` diff --git a/plugins/jest/src/index.ts b/plugins/jest/src/index.ts deleted file mode 100644 index 8a6730386..000000000 --- a/plugins/jest/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import JestCI from './tasks/ci' -import JestLocal from './tasks/local' - -export const tasks = [ - JestCI, - JestLocal -] diff --git a/plugins/jest/src/tasks/ci.ts b/plugins/jest/src/tasks/ci.ts deleted file mode 100644 index 06a8b95c2..000000000 --- a/plugins/jest/src/tasks/ci.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Task } from '@dotcom-tool-kit/types' -import { JestSchema } from '@dotcom-tool-kit/types/src/schema/jest' -import runJest from '../run-jest' - -export default class JestCI extends Task { - static description = '' - - async run(): Promise { - await runJest(this.logger, 'ci', this.options) - } -} diff --git a/plugins/jest/src/run-jest.ts b/plugins/jest/src/tasks/jest.ts similarity index 57% rename from plugins/jest/src/run-jest.ts rename to plugins/jest/src/tasks/jest.ts index 99ce6fbf5..c9f91d5cb 100644 --- a/plugins/jest/src/run-jest.ts +++ b/plugins/jest/src/tasks/jest.ts @@ -1,8 +1,9 @@ +import { Task } from '@dotcom-tool-kit/base' +import { JestSchema } from '@dotcom-tool-kit/schemas/lib/tasks/jest' import { fork } from 'node:child_process' import { readFile } from 'node:fs/promises' -import type { JestOptions, JestMode } from '@dotcom-tool-kit/types/lib/schema/jest' import { hookFork, waitOnExit } from '@dotcom-tool-kit/logger' -import type { Logger } from 'winston' + const jestCLIPath = require.resolve('jest-cli/bin/jest') // By default Jest will choose the number of worker threads based on the number @@ -28,23 +29,25 @@ async function guessVCpus(): Promise { } } -export default async function runJest(logger: Logger, mode: JestMode, options: JestOptions): Promise { - const args = ['--colors', options.configPath ? `--config=${options.configPath}` : ''] - // TODO:20231107:IM we should probably refactor this plugin to move - // CI-specific logic to be within the CI task module - if (mode === 'ci') { - args.push('--ci') - // only relevant if running on CircleCI, other CI environments might handle - // virtualisation completely differently - if (process.env.CIRCLECI) { - // leave one vCPU free for the main thread, same as the default Jest - // logic - const maxWorkers = (await guessVCpus()) - 1 - args.push(`--max-workers=${maxWorkers}`) +export default class Jest extends Task<{ task: typeof JestSchema }> { + async run(): Promise { + const args = ['--colors', this.options.configPath ? `--config=${this.options.configPath}` : ''] + + if (this.options.ci) { + args.push('--ci') + // only relevant if running on CircleCI, other CI environments might handle + // virtualisation completely differently + if (process.env.CIRCLECI) { + // leave one vCPU free for the main thread, same as the default Jest + // logic + const maxWorkers = (await guessVCpus()) - 1 + args.push(`--max-workers=${maxWorkers}`) + } } + + this.logger.info(`running jest ${args.join(' ')}`) + const child = fork(jestCLIPath, args, { silent: true }) + hookFork(this.logger, 'jest', child) + return waitOnExit('jest', child) } - logger.info(`running jest ${args.join(' ')}`) - const child = fork(jestCLIPath, args, { silent: true }) - hookFork(logger, 'jest', child) - return waitOnExit('jest', child) } diff --git a/plugins/jest/src/tasks/local.ts b/plugins/jest/src/tasks/local.ts deleted file mode 100644 index a17f09c29..000000000 --- a/plugins/jest/src/tasks/local.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Task } from '@dotcom-tool-kit/types' -import { JestSchema } from '@dotcom-tool-kit/types/src/schema/jest' -import runJest from '../run-jest' - -export default class JestLocal extends Task { - static description = '' - - async run(): Promise { - await runJest(this.logger, 'local', this.options) - } -} diff --git a/plugins/jest/tests/jest-plugin.test.ts b/plugins/jest/tests/jest-plugin.test.ts index 0edb1c7a9..188bd6789 100644 --- a/plugins/jest/tests/jest-plugin.test.ts +++ b/plugins/jest/tests/jest-plugin.test.ts @@ -1,10 +1,9 @@ import { fork } from 'node:child_process' -import JestLocal from '../src/tasks/local' -import JestCI from '../src/tasks/ci' +import Jest from '../src/tasks/jest' import EventEmitter from 'events' import winston, { Logger } from 'winston' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger jest.mock('node:child_process', () => ({ fork: jest.fn(() => { @@ -19,55 +18,36 @@ jest.mock('node:child_process', () => ({ jest.mock('@dotcom-tool-kit/logger') describe('jest plugin', () => { - describe('local', () => { - it('should call jest cli with configPath if configPath is passed in', async () => { - const jestLocal = new JestLocal(logger, { configPath: './src/jest.config.js' }) - await jestLocal.run() - - expect(fork).toBeCalledWith( - expect.any(String), - expect.arrayContaining(['--config=./src/jest.config.js']), - { - silent: true - } - ) - }) - - it('should call jest cli without configPath by default', async () => { - const jestLocal = new JestLocal(logger, {}) - await jestLocal.run() - - expect(fork).toBeCalledWith( - expect.any(String), - expect.not.arrayContaining([expect.stringContaining('--config')]), - { silent: true } - ) - }) + it('should call jest cli without configPath by default', async () => { + const jest = new Jest(logger, 'Jest', {}, {}) + await jest.run() + + expect(fork).toBeCalledWith( + expect.any(String), + expect.not.arrayContaining([expect.stringContaining('--config')]), + { silent: true } + ) }) - describe('ci', () => { - it('should call jest cli with configPath if configPath is passed in', async () => { - const jestCI = new JestCI(logger, { configPath: './src/jest.config.js' }) - await jestCI.run() - - expect(fork).toBeCalledWith( - expect.any(String), - expect.arrayContaining(['--ci', '--config=./src/jest.config.js']), - { - silent: true - } - ) - }) + it('should call jest cli with configPath if configPath is passed in', async () => { + const jest = new Jest(logger, 'Jest', {}, { configPath: './src/jest.config.js' }) + await jest.run() + + expect(fork).toBeCalledWith( + expect.any(String), + expect.arrayContaining(['--config=./src/jest.config.js']), + { + silent: true + } + ) + }) - it('should call jest cli without configPath by default', async () => { - const jestCI = new JestCI(logger, {}) - await jestCI.run() + it('should call jest cli with ci if ci is passed in', async () => { + const jest = new Jest(logger, 'Jest', {}, { ci: true }) + await jest.run() - expect(fork).toBeCalledWith( - expect.any(String), - expect.not.arrayContaining([expect.stringContaining('--config')]), - { silent: true } - ) + expect(fork).toBeCalledWith(expect.any(String), expect.arrayContaining(['--ci']), { + silent: true }) }) }) diff --git a/plugins/jest/tsconfig.json b/plugins/jest/tsconfig.json index e8bfc4e6d..29c8073d9 100644 --- a/plugins/jest/tsconfig.json +++ b/plugins/jest/tsconfig.json @@ -6,10 +6,13 @@ }, "references": [ { - "path": "../../lib/types" + "path": "../../lib/base" }, { "path": "../../lib/logger" + }, + { + "path": "../../lib/schemas" } ], "include": [ diff --git a/plugins/lint-staged-npm/.toolkitrc.yml b/plugins/lint-staged-npm/.toolkitrc.yml index 03647dfa9..ef04cfc58 100644 --- a/plugins/lint-staged-npm/.toolkitrc.yml +++ b/plugins/lint-staged-npm/.toolkitrc.yml @@ -1,3 +1,17 @@ plugins: - '@dotcom-tool-kit/lint-staged' - '@dotcom-tool-kit/husky-npm' + - '@dotcom-tool-kit/package-json-hook' + +options: + hooks: + - PackageJson: + lint-staged: + !toolkit/option '@dotcom-tool-kit/lint-staged-npm.testGlob': + commands: 'test:staged' + trailingString: '--' + !toolkit/option '@dotcom-tool-kit/lint-staged-npm.formatGlob': + commands: 'format:staged' + trailingString: '--' + +version: 2 diff --git a/plugins/lint-staged-npm/index.js b/plugins/lint-staged-npm/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/lint-staged-npm/jest.config.js b/plugins/lint-staged-npm/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/lint-staged-npm/jest.config.js +++ b/plugins/lint-staged-npm/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/lint-staged-npm/package.json b/plugins/lint-staged-npm/package.json index 72d45bdef..5552150e1 100644 --- a/plugins/lint-staged-npm/package.json +++ b/plugins/lint-staged-npm/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/lint-staged-npm", - "version": "3.2.0", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,9 +10,10 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/husky-npm": "^4.2.0", - "@dotcom-tool-kit/lint-staged": "^4.2.0", - "@dotcom-tool-kit/options": "^3.2.0", + "@dotcom-tool-kit/husky-npm": "5.0.0-beta.5", + "@dotcom-tool-kit/lint-staged": "5.0.0-beta.5", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", "tslib": "^2.3.1" }, "repository": { @@ -30,10 +31,10 @@ "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/lint-staged-npm/src/index.ts b/plugins/lint-staged-npm/src/index.ts deleted file mode 100644 index 7b4ccbee2..000000000 --- a/plugins/lint-staged-npm/src/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { LintStagedHook } from '@dotcom-tool-kit/lint-staged' -import { getOptions } from '@dotcom-tool-kit/options' - -class TestStaged extends LintStagedHook { - static description = 'test git staged files' - - _key?: string - get key(): string { - return (this._key ??= getOptions('@dotcom-tool-kit/lint-staged-npm')?.testGlob ?? '**/*.js') - } - hook = 'test:staged' -} - -class FormatStaged extends LintStagedHook { - static description = 'format git staged files' - - _key?: string - get key(): string { - return (this._key ??= getOptions('@dotcom-tool-kit/lint-staged-npm')?.formatGlob ?? '**/*.js') - } - hook = 'format:staged' -} - -export const hooks = { - 'test:staged': TestStaged, - 'format:staged': FormatStaged -} diff --git a/plugins/lint-staged/.toolkitrc.yml b/plugins/lint-staged/.toolkitrc.yml index 0fd58a55e..0efc6ac03 100644 --- a/plugins/lint-staged/.toolkitrc.yml +++ b/plugins/lint-staged/.toolkitrc.yml @@ -1,2 +1,7 @@ -hooks: +tasks: + LintStaged: './lib/tasks/lint-staged' + +commands: 'git:precommit': LintStaged + +version: 2 diff --git a/plugins/lint-staged/package.json b/plugins/lint-staged/package.json index a6be1f57d..44151d170 100644 --- a/plugins/lint-staged/package.json +++ b/plugins/lint-staged/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/lint-staged", - "version": "4.2.0", + "version": "5.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,9 +10,9 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/package-json-hook": "^4.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "lint-staged": "^11.2.3", "tslib": "^2.3.1" }, @@ -31,10 +31,10 @@ "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/lint-staged/readme.md b/plugins/lint-staged/readme.md index 8b9b06d5b..8df31aaec 100644 --- a/plugins/lint-staged/readme.md +++ b/plugins/lint-staged/readme.md @@ -51,12 +51,10 @@ flowchart G --> H[prettier index.js] ``` -## Options - -No options provided. - + ## Tasks -| Task | Description | Preconfigured hooks | -|-|-|-| -| `LintStaged` | run lint-staged on git staged files | `git:precommit` | +### `LintStaged` + +Run `lint-staged` in your repo, for use with git hooks. + diff --git a/plugins/lint-staged/src/hook.ts b/plugins/lint-staged/src/hook.ts deleted file mode 100644 index ccb70d9c5..000000000 --- a/plugins/lint-staged/src/hook.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PackageJsonHelper } from '@dotcom-tool-kit/package-json-hook' - -export abstract class LintStagedHook extends PackageJsonHelper { - field = 'lint-staged' - trailingString = '--' -} diff --git a/plugins/lint-staged/src/index.ts b/plugins/lint-staged/src/index.ts deleted file mode 100644 index ffca0f886..000000000 --- a/plugins/lint-staged/src/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import LintStaged from './tasks/lint-staged' - -export const tasks = [LintStaged] -export { LintStagedHook } from './hook' diff --git a/plugins/lint-staged/src/tasks/lint-staged.ts b/plugins/lint-staged/src/tasks/lint-staged.ts index e721560c5..281e439a7 100644 --- a/plugins/lint-staged/src/tasks/lint-staged.ts +++ b/plugins/lint-staged/src/tasks/lint-staged.ts @@ -1,6 +1,6 @@ import { ToolKitError } from '@dotcom-tool-kit/error' import { hookConsole } from '@dotcom-tool-kit/logger' -import { Task } from '@dotcom-tool-kit/types' +import { Task } from '@dotcom-tool-kit/base' import lintStaged from 'lint-staged' export default class LintStaged extends Task { diff --git a/plugins/lint-staged/tsconfig.json b/plugins/lint-staged/tsconfig.json index 04dfa9bd0..4add90e55 100644 --- a/plugins/lint-staged/tsconfig.json +++ b/plugins/lint-staged/tsconfig.json @@ -6,7 +6,7 @@ }, "references": [ { - "path": "../../lib/package-json-hook" + "path": "../../plugins/package-json-hook" }, { "path": "../../lib/logger" diff --git a/plugins/mocha/.toolkitrc.yml b/plugins/mocha/.toolkitrc.yml index bf812f6d4..0c788aeb6 100644 --- a/plugins/mocha/.toolkitrc.yml +++ b/plugins/mocha/.toolkitrc.yml @@ -1,3 +1,8 @@ -hooks: +tasks: + Mocha: './lib/tasks/mocha' + +commands: 'test:local': Mocha 'test:ci': Mocha + +version: 2 diff --git a/plugins/mocha/jest.config.js b/plugins/mocha/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/mocha/jest.config.js +++ b/plugins/mocha/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/mocha/package.json b/plugins/mocha/package.json index 8de061c47..86362a0fc 100644 --- a/plugins/mocha/package.json +++ b/plugins/mocha/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/mocha", - "version": "3.2.0", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,9 +10,9 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "glob": "^7.1.7", "tslib": "^2.3.1" }, @@ -24,6 +24,7 @@ "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/mocha", "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "@types/glob": "^7.1.3", "@types/mocha": "^8.2.2", @@ -37,11 +38,11 @@ "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x", + "dotcom-tool-kit": "4.0.0-beta.5", "mocha": ">=6.x <=10.x" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/mocha/readme.md b/plugins/mocha/readme.md index 03d21e3a9..db12323cf 100644 --- a/plugins/mocha/readme.md +++ b/plugins/mocha/readme.md @@ -17,27 +17,30 @@ plugins: - '@dotcom-tool-kit/mocha' ``` -## Options + +## Tasks -| Key | Description | Default value | -|-|-|-| -| `files` | A file path glob to Mocha tests | `'test/**/*.js'` | -| `configPath` | Path to the [Mocha config file](https://mochajs.org/#configuring-mocha-nodejs) | use Mocha's own [config resolution](https://mochajs.org/#priorities) | +### `Mocha` -## Tasks +Runs `mocha` to execute tests. +#### Task options + +| Property | Description | Type | Default | +| :----------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :--------------- | +| `files` | A file path glob to Mocha tests. | `string` | `'test/**/*.js'` | +| `configPath` | Path to the [Mocha config file](https://mochajs.org/#configuring-mocha-nodejs). Uses Mocha's own [config resolution](https://mochajs.org/#priorities) by default. | `string` | | -| Task | Description | Preconfigured hook | -|-|-|-| -| `Mocha` | runs `mocha` to execute tests | `test:local`, `test:ci` | +_All properties are optional._ + ## Tips -### Resolving test hook conflicts +### Resolving test command conflicts -A common use case is to configure `test:local` and `test:ci` in your `.toolkitrc.yml` to run the `Eslint` task then the relevant Mocha task: +A common use case is to configure `test:local` and `test:ci` in your `.toolkitrc.yml` to run the `Eslint` task then the relevant Mocha task: ```yaml -hooks: +commands: test:local: - Eslint - Mocha diff --git a/plugins/mocha/src/index.ts b/plugins/mocha/src/index.ts deleted file mode 100644 index 49e8daba9..000000000 --- a/plugins/mocha/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Mocha from './tasks/mocha' - -export const tasks = [Mocha] diff --git a/plugins/mocha/src/tasks/mocha.ts b/plugins/mocha/src/tasks/mocha.ts index ce2a6929b..31bf84139 100644 --- a/plugins/mocha/src/tasks/mocha.ts +++ b/plugins/mocha/src/tasks/mocha.ts @@ -1,14 +1,12 @@ import { hookFork, waitOnExit } from '@dotcom-tool-kit/logger' -import { Task } from '@dotcom-tool-kit/types' +import { Task } from '@dotcom-tool-kit/base' import { glob } from 'glob' -import { MochaSchema } from '@dotcom-tool-kit/types/lib/schema/mocha' +import { MochaSchema } from '@dotcom-tool-kit/schemas/lib/tasks/mocha' import { fork } from 'child_process' import { promisify } from 'util' const mochaCLIPath = require.resolve('mocha/bin/mocha') -export default class Mocha extends Task { - static description = '' - +export default class Mocha extends Task<{ task: typeof MochaSchema }> { async run(): Promise { const files = await promisify(glob)(this.options.files) diff --git a/plugins/mocha/test/tasks/mocha.test.ts b/plugins/mocha/test/tasks/mocha.test.ts index c22e83788..b97893ed4 100644 --- a/plugins/mocha/test/tasks/mocha.test.ts +++ b/plugins/mocha/test/tasks/mocha.test.ts @@ -5,21 +5,31 @@ import * as path from 'path' import Mocha from '../../src/tasks/mocha' import winston, { Logger } from 'winston' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger describe('mocha', () => { it('should succeed with passing tests', async () => { - const task = new Mocha(logger, { - files: path.resolve(__dirname, '../files/pass') + '/**/*.js' - }) + const task = new Mocha( + logger, + 'Mocha', + {}, + { + files: path.resolve(__dirname, '../files/pass') + '/**/*.js' + } + ) await task.run() }) it('should throw with failing tests', async () => { - const task = new Mocha(logger, { - files: path.resolve(__dirname, '../files/fail') + '/**/*.js' - }) + const task = new Mocha( + logger, + 'Mocha', + {}, + { + files: path.resolve(__dirname, '../files/fail') + '/**/*.js' + } + ) expect.assertions(1) try { diff --git a/plugins/mocha/tsconfig.json b/plugins/mocha/tsconfig.json index 84d248bcc..8beb1c2e5 100644 --- a/plugins/mocha/tsconfig.json +++ b/plugins/mocha/tsconfig.json @@ -5,13 +5,21 @@ "path": "../../lib/logger" }, { - "path": "../../lib/types" + "path": "../../lib/base" + }, + { + "path": "../../lib/schemas" } ], - "include": ["src/**/*"], + "include": [ + "src/**/*" + ], "compilerOptions": { "outDir": "lib", "rootDir": "src", - "types": ["node", "mocha"] + "types": [ + "node", + "mocha" + ] } } diff --git a/plugins/n-test/.toolkitrc.yml b/plugins/n-test/.toolkitrc.yml index bc99b350f..723cf56c4 100644 --- a/plugins/n-test/.toolkitrc.yml +++ b/plugins/n-test/.toolkitrc.yml @@ -1,3 +1,8 @@ -hooks: +tasks: + NTest: './lib/tasks/n-test' + +commands: 'test:review': NTest 'test:staging': NTest + +version: 2 diff --git a/plugins/n-test/README.md b/plugins/n-test/README.md index 67649372c..ebf1ae2f5 100644 --- a/plugins/n-test/README.md +++ b/plugins/n-test/README.md @@ -17,18 +17,21 @@ plugins: - '@dotcom-tool-kit/n-test' ``` -## Options + +## Tasks -| Key | Description | Default value | -|-|-|-| -| `browsers` | Array; Selenium browsers to run the test against | | -| `host` | Set the hostname to use for all tests | | -| `config` | Path to config file used to test | './test/smoke.js' | -| `interactive` | Boolean; interactively choose which tests to run | `false` | -| `header` | Request headers to be sent with every request. e.g. "X-Api-Key: 1234" | | +### `NTest` -## Tasks +Run [n-test](https://github.com/financial-times/n-test) smoke tests against your application. +#### Task options + +| Property | Description | Type | +| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------- | +| `browsers` | Selenium browsers to run the test against | `Array` | +| `host` | Set the hostname to use for all tests. If running in an environment such as a review or staging app build that has Tool Kit state with a URL for an app to run against, that will override this option. | `string` | +| `config` | Path to config file used to test | `string` | +| `interactive` | Interactively choose which tests to run | `boolean` | +| `header` | Request headers to be sent with every request | `Record` | -| Task | Description | Preconfigured hooks | -|-|-|-| -| `NTest` | runs smoke tests as part of your CircleCi workflow | `test:review`, `test:staging` | +_All properties are optional._ + diff --git a/plugins/n-test/jest.config.js b/plugins/n-test/jest.config.js index e621b81cb..9fbb18e03 100644 --- a/plugins/n-test/jest.config.js +++ b/plugins/n-test/jest.config.js @@ -1,14 +1,15 @@ const base = require('../../jest.config.base') +const path = require('path') module.exports = { - ...base, - globals: { - 'ts-jest': { - tsconfig: { - paths: { - puppeteer: ['__mocks__/puppeteer'] - } + ...base.config, + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + ...base.tsJestConfig, + tsconfig: path.resolve(__dirname, './tsconfig.test.json') } - } + ] } } diff --git a/plugins/n-test/package.json b/plugins/n-test/package.json index 19a8b1a97..83b1fd367 100644 --- a/plugins/n-test/package.json +++ b/plugins/n-test/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/n-test", - "version": "3.3.1", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,9 +10,9 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/state": "^3.3.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "@financial-times/n-test": "^6.1.0-beta.1", "tslib": "^2.3.1" }, @@ -24,6 +24,7 @@ "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/n-test", "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "@types/jest": "^27.4.0", "winston": "^3.5.1" @@ -36,10 +37,10 @@ "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/n-test/src/index.ts b/plugins/n-test/src/index.ts deleted file mode 100644 index 3eaa19d0d..000000000 --- a/plugins/n-test/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import NTest from './tasks/n-test' - -export const tasks = [NTest] diff --git a/plugins/n-test/src/tasks/n-test.ts b/plugins/n-test/src/tasks/n-test.ts index 1d9a02163..f9569c274 100644 --- a/plugins/n-test/src/tasks/n-test.ts +++ b/plugins/n-test/src/tasks/n-test.ts @@ -1,12 +1,10 @@ import { styles } from '@dotcom-tool-kit/logger' -import { Task } from '@dotcom-tool-kit/types' -import { SmokeTestSchema } from '@dotcom-tool-kit/types/lib/schema/n-test' +import { Task } from '@dotcom-tool-kit/base' +import { SmokeTestSchema } from '@dotcom-tool-kit/schemas/lib/tasks/n-test' import { SmokeTest } from '@financial-times/n-test' import { readState } from '@dotcom-tool-kit/state' -export default class NTest extends Task { - static description = '' - +export default class NTest extends Task<{ task: typeof SmokeTestSchema }> { async run(): Promise { const appState = readState('review') ?? readState('staging') @@ -14,8 +12,7 @@ export default class NTest extends Task { if (appState) { // HACK:20231003:IM keep the old logic of using the app name as the // subdomain to maintain backwards compatibility - this.options.host = - 'url' in appState && appState.url ? appState.url : `https://${appState.appName}.herokuapp.com` + this.options.host = appState.url ? appState.url : `https://${appState.appName}.herokuapp.com` // HACK:20231003:IM n-test naively appends paths to the host URL so // expects there to be no trailing slash if (this.options.host.endsWith('/')) { diff --git a/plugins/n-test/test/tasks/n-test.test.ts b/plugins/n-test/test/tasks/n-test.test.ts index 16bf80c2b..bc53128a1 100644 --- a/plugins/n-test/test/tasks/n-test.test.ts +++ b/plugins/n-test/test/tasks/n-test.test.ts @@ -5,7 +5,7 @@ import NTest from '../../src/tasks/n-test' import { writeState } from '@dotcom-tool-kit/state' import winston, { Logger } from 'winston' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger const configAbsolutePath = path.join(__dirname, '../files/smoke.js') // n-test prepends the CWD to the given config path @@ -13,17 +13,27 @@ const configPath = path.relative('', configAbsolutePath) describe('n-test', () => { it('should pass when no errors', async () => { - const task = new NTest(logger, { - config: configPath - }) + const task = new NTest( + logger, + 'NTest', + {}, + { + config: configPath + } + ) await task.run() }) it('should fail when there are errors', async () => { - const task = new NTest(logger, { - config: configPath - }) + const task = new NTest( + logger, + 'NTest', + {}, + { + config: configPath + } + ) puppeteer.__setResponseStatus(404) @@ -38,9 +48,14 @@ describe('n-test', () => { it('should get app url from state', async () => { const appUrl = 'https://some-test-app.herokuapp.com' writeState('review', { url: appUrl }) - const task = new NTest(logger, { - config: configPath - }) + const task = new NTest( + logger, + 'NTest', + {}, + { + config: configPath + } + ) try { await task.run() diff --git a/plugins/n-test/tsconfig.json b/plugins/n-test/tsconfig.json index b6d36b494..392329b3c 100644 --- a/plugins/n-test/tsconfig.json +++ b/plugins/n-test/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.settings.json", - "include": ["src/**/*"], + "include": [ + "src/**/*" + ], "compilerOptions": { "outDir": "lib", "rootDir": "src" @@ -13,7 +15,10 @@ "path": "../../lib/logger" }, { - "path": "../../lib/types" + "path": "../../lib/base" + }, + { + "path": "../../lib/schemas" } ] } diff --git a/plugins/n-test/tsconfig.test.json b/plugins/n-test/tsconfig.test.json new file mode 100644 index 000000000..b479ffc0f --- /dev/null +++ b/plugins/n-test/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "paths": { + "puppeteer": ["__mocks__/puppeteer"] + } + } +} diff --git a/plugins/next-router/.toolkitrc.yml b/plugins/next-router/.toolkitrc.yml index aa4680411..c18468860 100644 --- a/plugins/next-router/.toolkitrc.yml +++ b/plugins/next-router/.toolkitrc.yml @@ -1,6 +1,11 @@ plugins: - '@dotcom-tool-kit/doppler' -hooks: +tasks: + NextRouter: './lib/tasks/next-router' + +commands: run:local: - NextRouter + +version: 2 diff --git a/plugins/next-router/package.json b/plugins/next-router/package.json index 0f220e3e9..ede0293d8 100644 --- a/plugins/next-router/package.json +++ b/plugins/next-router/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/next-router", - "version": "3.4.1", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,11 +10,11 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/state": "^3.3.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", - "@dotcom-tool-kit/doppler": "^1.1.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "ft-next-router": "^3.0.0", "tslib": "^2.3.1" }, @@ -30,10 +30,13 @@ ".toolkitrc.yml" ], "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" + }, + "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0" } } diff --git a/plugins/next-router/readme.md b/plugins/next-router/readme.md index 40f7345c9..422a4fe8a 100644 --- a/plugins/next-router/readme.md +++ b/plugins/next-router/readme.md @@ -19,7 +19,7 @@ plugins: ## Running an app via next-router -For an app to be loaded via the `next-router` (https://local.ft.com:5050), the app will need to be loaded before the `next-router` plugin. This is done by defining the `run:local` hook to run the application before `NextRouter` task. Here is an example full config to do that: +For an app to be loaded via the `next-router` (https://local.ft.com:5050), the app will need to be loaded before the `next-router` plugin. This is done by assigning the `run:local` command to run the application before `NextRouter` task. Here is an example full config to do that: ```yml plugins: @@ -27,7 +27,7 @@ plugins: - '@dotcom-tool-kit/nodemon' - '@dotcom-tool-kit/doppler' -hooks: +commands: run:local: - Nodemon - NextRouter @@ -39,14 +39,14 @@ options: project: '[systemCode]' # corresponding doppler project name ``` -## Options + +## Plugin-wide options -| Key | Description | Default value | Required | -|-|-|-|-| -| `appName` | the system's `name` field as it appears in [next-service-registry](https://next-registry.ft.com/v2), which is _often different to its `code` value so be sure to check_) | | ✅ | +### `@dotcom-tool-kit/next-router` -## Tasks +| Property | Description | Type | +| :----------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | +| **`appName`** (\*) | The system's `name` field as it appears in [next-service-registry](https://next-registry.ft.com/v2). **This is often different to its Biz Ops system code**, so be sure to check. | `string` | -| Task | Description | Default hooks | -|-|-|-| -| `NextRouter` | Run the application via the `next-router` | `run:local` | +_(\*) Required._ + diff --git a/plugins/next-router/src/index.ts b/plugins/next-router/src/index.ts deleted file mode 100644 index 6eda59f4b..000000000 --- a/plugins/next-router/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import NextRouter from './tasks/next-router' - -export const tasks = [NextRouter] diff --git a/plugins/next-router/src/tasks/next-router.ts b/plugins/next-router/src/tasks/next-router.ts index 6b2ac5654..d5f7764bf 100644 --- a/plugins/next-router/src/tasks/next-router.ts +++ b/plugins/next-router/src/tasks/next-router.ts @@ -1,15 +1,13 @@ -import { Task } from '@dotcom-tool-kit/types' +import { Task } from '@dotcom-tool-kit/base' import { DopplerEnvVars } from '@dotcom-tool-kit/doppler' import { register } from 'ft-next-router' import { readState } from '@dotcom-tool-kit/state' import { hookConsole, hookFork, styles, waitOnExit } from '@dotcom-tool-kit/logger' import { ToolKitError } from '@dotcom-tool-kit/error' import { fork } from 'child_process' -import { NextRouterSchema } from '@dotcom-tool-kit/types/lib/schema/next-router' - -export default class NextRouter extends Task { - static description = '' +import { NextRouterSchema } from '@dotcom-tool-kit/schemas/lib/plugins/next-router' +export default class NextRouter extends Task<{ plugin: typeof NextRouterSchema }> { async run(): Promise { const doppler = new DopplerEnvVars(this.logger, 'dev', { project: 'next-router' @@ -38,7 +36,7 @@ export default class NextRouter extends Task { const unhook = hookConsole(this.logger, 'ft-next-router') try { - await register({ service: this.options.appName, port: local.port }) + await register({ service: this.pluginOptions.appName, port: local.port }) } finally { unhook() } diff --git a/plugins/next-router/tsconfig.json b/plugins/next-router/tsconfig.json index 8af41e6e2..e77e250c9 100644 --- a/plugins/next-router/tsconfig.json +++ b/plugins/next-router/tsconfig.json @@ -15,11 +15,16 @@ "path": "../../lib/logger" }, { - "path": "../../lib/types" + "path": "../../lib/base" }, { "path": "../../lib/doppler" + }, + { + "path": "../../lib/schemas" } ], - "include": ["src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/plugins/node/.toolkitrc.yml b/plugins/node/.toolkitrc.yml index 025a1ef46..5e2203766 100644 --- a/plugins/node/.toolkitrc.yml +++ b/plugins/node/.toolkitrc.yml @@ -1,5 +1,10 @@ plugins: - '@dotcom-tool-kit/doppler' -hooks: +tasks: + Node: './lib/tasks/node' + +commands: 'run:local': Node + +version: 2 diff --git a/plugins/node/package.json b/plugins/node/package.json index 5108917c4..847a90946 100644 --- a/plugins/node/package.json +++ b/plugins/node/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/node", - "version": "3.4.1", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,10 +10,10 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/state": "^3.3.0", - "@dotcom-tool-kit/types": "^3.6.0", - "@dotcom-tool-kit/doppler": "^1.1.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", "get-port": "^5.1.1", "tslib": "^2.3.1", "wait-port": "^0.2.9" @@ -30,10 +30,13 @@ ".toolkitrc.yml" ], "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" + }, + "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0" } } diff --git a/plugins/node/readme.md b/plugins/node/readme.md index e78f3d3d8..383023af7 100644 --- a/plugins/node/readme.md +++ b/plugins/node/readme.md @@ -19,17 +19,20 @@ plugins: - '@dotcom-tool-kit/node' ``` -## Options + +## Tasks -| Key | Description | Default value | -|-|-|-| -| `entry` | path to the node application | `'./server/app.js'` | -| `args` | additional arguments to pass to your application | `[]` | -| `useVault` | option to run the application with environment variables from Vault | `true` | -| `ports` | ports to try to bind to for this application | `[3001, 3002, 3003]` | +### `Node` -## Tasks +Run a Node.js application for local development. +#### Task options + +| Property | Description | Type | Default | +| :----------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------- | :------------------ | +| `entry` | path to the node application | `string` | `'./server/app.js'` | +| `args` | additional arguments to pass to your application | `Array` | | +| `useDoppler` | whether to run the application with environment variables from Doppler | `boolean` | `true` | +| `ports` | ports to try to bind to for this application. set to `false` for an entry point that wouldn't bind to a port, such as a worker process or one-off script. | `Array \| false` | `[3001,3002,3003]` | -| Task | Description | Default hooks | -|-|-|-| -| `Node` | Run node application | `run:local` | +_All properties are optional._ + diff --git a/plugins/node/src/index.ts b/plugins/node/src/index.ts deleted file mode 100644 index 2cda603a7..000000000 --- a/plugins/node/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Node from './tasks/node' - -export const tasks = [Node] diff --git a/plugins/node/src/tasks/node.ts b/plugins/node/src/tasks/node.ts index 2fccd3f67..a9fa54def 100644 --- a/plugins/node/src/tasks/node.ts +++ b/plugins/node/src/tasks/node.ts @@ -1,62 +1,65 @@ -import { ToolKitError } from '@dotcom-tool-kit/error' -import { hookConsole, hookFork, styles } from '@dotcom-tool-kit/logger' +import { hookConsole, hookFork, waitOnExit } from '@dotcom-tool-kit/logger' import { writeState } from '@dotcom-tool-kit/state' -import { Task } from '@dotcom-tool-kit/types' -import { NodeSchema } from '@dotcom-tool-kit/types/lib/schema/node' +import { Task } from '@dotcom-tool-kit/base' +import { NodeSchema } from '@dotcom-tool-kit/schemas/lib/tasks/node' import { DopplerEnvVars } from '@dotcom-tool-kit/doppler' -import { fork } from 'child_process' +import { ChildProcess, fork } from 'child_process' import getPort from 'get-port' import waitPort from 'wait-port' -export default class Node extends Task { - static description = '' +export default class Node extends Task<{ task: typeof NodeSchema }> { + child?: ChildProcess async run(): Promise { - const { entry, args, useVault, ports } = this.options + const { entry, args, useDoppler, ports } = this.options - let vaultEnv = {} + let dopplerEnv = {} - if (useVault) { - const vault = new DopplerEnvVars(this.logger, 'dev') + if (useDoppler) { + const doppler = new DopplerEnvVars(this.logger, 'dev') - vaultEnv = await vault.get() + dopplerEnv = await doppler.get() } - const port = - Number(process.env.PORT) || - (await getPort({ - port: ports - })) - - if (!entry) { - const error = new ToolKitError( - `the ${styles.task('Node')} task requires an ${styles.option('entry')} option` - ) - error.details = `this is the entrypoint for your app, e.g. ${styles.filepath('server/app.js')}` - throw error - } + const port = ports + ? Number(process.env.PORT) || + (await getPort({ + port: ports + })) + : false this.logger.verbose('starting the child node process...') - const child = fork(entry, args, { + this.child = fork(entry, args, { env: { - ...vaultEnv, + ...dopplerEnv, PORT: port.toString(), ...process.env }, silent: true }) - hookFork(this.logger, entry, child) - - const unhook = hookConsole(this.logger, 'wait-port') - try { - await waitPort({ - host: 'localhost', - port: port - }) - } finally { - unhook() + hookFork(this.logger, entry, this.child) + + if (port) { + const unhook = hookConsole(this.logger, 'wait-port') + try { + await waitPort({ + host: 'localhost', + port + }) + } finally { + unhook() + } + + writeState('local', { port }) } - writeState('local', { port }) + await waitOnExit('node', this.child) + } + + async stop() { + if (this.child && (this.child.exitCode === null || !this.child.killed)) { + // SIGINT instead of SIGKILL so the process gets chance to exit gracefully + this.child.kill('SIGINT') + } } } diff --git a/plugins/node/tsconfig.json b/plugins/node/tsconfig.json index 164487c20..be559dc10 100644 --- a/plugins/node/tsconfig.json +++ b/plugins/node/tsconfig.json @@ -6,7 +6,7 @@ }, "references": [ { - "path": "../../lib/types" + "path": "../../lib/base" }, { "path": "../../lib/error" @@ -19,7 +19,12 @@ }, { "path": "../../lib/state" + }, + { + "path": "../../lib/schemas" } ], - "include": ["src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/plugins/nodemon/.toolkitrc.yml b/plugins/nodemon/.toolkitrc.yml index 495274862..5611627df 100644 --- a/plugins/nodemon/.toolkitrc.yml +++ b/plugins/nodemon/.toolkitrc.yml @@ -1,5 +1,10 @@ plugins: - '@dotcom-tool-kit/doppler' -hooks: +tasks: + Nodemon: './lib/tasks/nodemon' + +commands: 'run:local': Nodemon + +version: 2 diff --git a/plugins/nodemon/package.json b/plugins/nodemon/package.json index 34baf961e..6cf4ad5d0 100644 --- a/plugins/nodemon/package.json +++ b/plugins/nodemon/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/nodemon", - "version": "3.4.1", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,16 +10,16 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/state": "^3.3.0", - "@dotcom-tool-kit/types": "^3.6.0", - "@dotcom-tool-kit/doppler": "^1.1.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", "get-port": "^5.1.1", "tslib": "^2.3.1" }, "peerDependencies": { - "nodemon": "2.x", - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5", + "nodemon": "2.x" }, "repository": { "type": "git", @@ -33,10 +33,11 @@ ".toolkitrc.yml" ], "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@types/nodemon": "^1.19.1" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/nodemon/readme.md b/plugins/nodemon/readme.md index 94e3a3344..779b36cab 100644 --- a/plugins/nodemon/readme.md +++ b/plugins/nodemon/readme.md @@ -17,17 +17,20 @@ plugins: - '@dotcom-tool-kit/nodemon' ``` -## Options + +## Tasks -| Key | Description | Default value | -|-|-|-| -| `entry` | path to the node application | `'./server/app.js'` | -| `configPath` | path to custom nodemon config | [automatic config resolution](https://github.com/remy/nodemon#config-files) | -| `useVault` | option to run the application with environment variables from Vault | `true` | -| `ports` | ports to try to bind to for this application | `[3001, 3002, 3003]` | +### `Nodemon` -## Tasks +Run an application with `nodemon` for local development. +#### Task options + +| Property | Description | Type | Default | +| :----------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------- | :------------------ | +| `entry` | path to the node application | `string` | `'./server/app.js'` | +| `configPath` | path to a Nodemon config file. defaults to Nodemon's [automatic config resolution](https://github.com/remy/nodemon#config-files). | `string` | | +| `useDoppler` | whether to run the application with environment variables from Doppler | `boolean` | `true` | +| `ports` | ports to try to bind to for this application. set to `false` for an entry point that wouldn't bind to a port, such as a worker process or one-off script. | `Array \| false` | `[3001,3002,3003]` | -| Task | Description | Default hooks | -|-|-|-| -| `Nodemon` | Run application with `nodemon` | `run:local` | +_All properties are optional._ + diff --git a/plugins/nodemon/src/index.ts b/plugins/nodemon/src/index.ts deleted file mode 100644 index 4fefb8d8e..000000000 --- a/plugins/nodemon/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Nodemon from './tasks/nodemon' - -export const tasks = [Nodemon] diff --git a/plugins/nodemon/src/tasks/nodemon.ts b/plugins/nodemon/src/tasks/nodemon.ts index 93f25dff0..66f459c31 100644 --- a/plugins/nodemon/src/tasks/nodemon.ts +++ b/plugins/nodemon/src/tasks/nodemon.ts @@ -1,6 +1,6 @@ import { hookFork } from '@dotcom-tool-kit/logger' -import { Task } from '@dotcom-tool-kit/types' -import { NodemonSchema } from '@dotcom-tool-kit/types/lib/schema/nodemon' +import { Task } from '@dotcom-tool-kit/base' +import { NodemonSchema } from '@dotcom-tool-kit/schemas/lib/tasks/nodemon' import { writeState } from '@dotcom-tool-kit/state' import { DopplerEnvVars } from '@dotcom-tool-kit/doppler' import getPort from 'get-port' @@ -8,25 +8,24 @@ import nodemon from 'nodemon' import { Readable } from 'stream' import { shouldDisableNativeFetch } from 'dotcom-tool-kit' -export default class Nodemon extends Task { - static description = '' - +export default class Nodemon extends Task<{ task: typeof NodemonSchema }> { async run(): Promise { - const { entry, configPath, useVault, ports } = this.options + const { entry, configPath, useDoppler, ports } = this.options let dopplerEnv = {} - if (useVault) { + if (useDoppler) { const doppler = new DopplerEnvVars(this.logger, 'dev') dopplerEnv = await doppler.get() } - const port = - Number(process.env.PORT) || - (await getPort({ - port: ports - })) + const port = ports + ? Number(process.env.PORT) || + (await getPort({ + port: ports + })) + : false this.logger.verbose('starting the child nodemon process...') @@ -44,7 +43,7 @@ export default class Nodemon extends Task { nodemon(config) nodemon.on('readable', () => { // These fields aren't specified in the type declaration for some reason - const { stdout, stderr } = (nodemon as unknown) as { stdout: Readable; stderr: Readable } + const { stdout, stderr } = nodemon as unknown as { stdout: Readable; stderr: Readable } hookFork(this.logger, entry, { stdout, stderr }) }) const nodemonLogger = this.logger.child({ process: 'nodemon' }) @@ -68,6 +67,7 @@ export default class Nodemon extends Task { nodemonLogger.log(nodemonToWinstonLogLevel(msg.type), msg.message + '\n') }) await new Promise((resolve) => nodemon.on('start', resolve)) - writeState('local', { port }) + + if (port) writeState('local', { port }) } } diff --git a/plugins/nodemon/tsconfig.json b/plugins/nodemon/tsconfig.json index ec6f98bae..86abc26c2 100644 --- a/plugins/nodemon/tsconfig.json +++ b/plugins/nodemon/tsconfig.json @@ -6,7 +6,10 @@ }, "references": [ { - "path": "../../lib/types" + "path": "../../core/cli" + }, + { + "path": "../../lib/base" }, { "path": "../../lib/error" @@ -19,6 +22,9 @@ }, { "path": "../../lib/state" + }, + { + "path": "../../lib/schemas" } ], "include": ["src/**/*"] diff --git a/plugins/npm/.toolkitrc.yml b/plugins/npm/.toolkitrc.yml index e69de29bb..cf6fb4c1b 100644 --- a/plugins/npm/.toolkitrc.yml +++ b/plugins/npm/.toolkitrc.yml @@ -0,0 +1,16 @@ +plugins: + - '@dotcom-tool-kit/package-json-hook' + +tasks: + NpmPrune: './lib/tasks/prune' + NpmPublish: './lib/tasks/publish' + +options: + hooks: + - PackageJson: + scripts: + build: 'build:local' + test: 'test:local' + start: 'run:local' + +version: 2 diff --git a/plugins/npm/jest.config.js b/plugins/npm/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/npm/jest.config.js +++ b/plugins/npm/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/npm/package.json b/plugins/npm/package.json index e087b4741..526524bb6 100644 --- a/plugins/npm/package.json +++ b/plugins/npm/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/npm", - "version": "3.3.1", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -11,10 +11,10 @@ "license": "ISC", "dependencies": { "@actions/exec": "^1.1.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/package-json-hook": "^4.2.0", - "@dotcom-tool-kit/state": "^3.3.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "libnpmpack": "^3.1.0", "libnpmpublish": "^5.0.1", "pacote": "^12.0.3", @@ -29,9 +29,10 @@ "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/npm", "devDependencies": { - "@types/libnpmpublish": "^4.0.1", + "@npm/types": "^1.0.2", + "@types/libnpmpublish": "^4.0.6", "@types/pacote": "^11.1.3", - "@types/tar": "^6.1.1", + "@types/tar": "^6.1.10", "winston": "^3.5.1" }, "files": [ @@ -42,10 +43,10 @@ "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/npm/readme.md b/plugins/npm/readme.md index 02b5e7c50..04679f5a9 100644 --- a/plugins/npm/readme.md +++ b/plugins/npm/readme.md @@ -19,17 +19,14 @@ plugins: - '@dotcom-tool-kit/npm' ``` -And install this plugin's hooks: + +## Tasks -```sh -npx dotcom-tool-kit --install -``` +### `NpmPrune` -This will modify your `package.json`. You should commit this change. +Prune development npm dependencies. -## Hooks +### `NpmPublish` -| Event | Description | Installed to...| -|-|-|-| -| `build:local` | Compile any assets or code required for your app to run locally, in development. | `build` script in `package.json` (i.e. run from `npm run build`) | -| `test:local` | Run your app's test suite locally, during development. | `test` script in `package.json` (i.e. run from `npm run test`) +Publish package to the npm registry. + diff --git a/plugins/npm/src/index.ts b/plugins/npm/src/index.ts deleted file mode 100644 index 59fbb25e7..000000000 --- a/plugins/npm/src/index.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { PackageJsonScriptHook } from '@dotcom-tool-kit/package-json-hook' -import NpmPrune from './tasks/npm-prune' -import NpmPublish from './tasks/npm-publish' - -class BuildLocal extends PackageJsonScriptHook { - static description = 'hook for `npm run build`, for building an app locally' - - key = 'build' - hook = 'build:local' -} - -class TestLocal extends PackageJsonScriptHook { - static description = 'hook for `npm run test`, for running tests locally' - - key = 'test' - hook = 'test:local' -} - -class RunLocal extends PackageJsonScriptHook { - static description = 'hook for `npm start`, for running your app locally' - - key = 'start' - hook = 'run:local' -} - -export { PackageJsonScriptHook as PackageJsonScriptHook } - -export const hooks = { - 'build:local': BuildLocal, - 'test:local': TestLocal, - 'run:local': RunLocal -} - -export const tasks = [ - NpmPrune, - NpmPublish -] diff --git a/plugins/npm/src/tasks/npm-prune.ts b/plugins/npm/src/tasks/prune.ts similarity index 87% rename from plugins/npm/src/tasks/npm-prune.ts rename to plugins/npm/src/tasks/prune.ts index 03a29de19..a821f7aa0 100644 --- a/plugins/npm/src/tasks/npm-prune.ts +++ b/plugins/npm/src/tasks/prune.ts @@ -1,10 +1,8 @@ -import { Task } from '@dotcom-tool-kit/types' +import { Task } from '@dotcom-tool-kit/base' import { ToolKitError } from '@dotcom-tool-kit/error' import * as exec from '@actions/exec' export default class NpmPrune extends Task { - static description = '' - async run(): Promise { try { this.logger.verbose('pruning dev dependencies...') diff --git a/plugins/npm/src/tasks/npm-publish.ts b/plugins/npm/src/tasks/publish.ts similarity index 72% rename from plugins/npm/src/tasks/npm-publish.ts rename to plugins/npm/src/tasks/publish.ts index 9796bf8ea..3813b2414 100644 --- a/plugins/npm/src/tasks/npm-publish.ts +++ b/plugins/npm/src/tasks/publish.ts @@ -1,7 +1,6 @@ import { readFile, writeFile } from 'fs/promises' import { ToolKitError } from '@dotcom-tool-kit/error' -import { Task } from '@dotcom-tool-kit/types' -import { semVerRegex, prereleaseRegex, releaseRegex } from '@dotcom-tool-kit/types/lib/npm' +import { Task } from '@dotcom-tool-kit/base' import pacote from 'pacote' import { readState } from '@dotcom-tool-kit/state' import pack from 'libnpmpack' @@ -9,16 +8,19 @@ import { publish } from 'libnpmpublish' import { styles } from '@dotcom-tool-kit/logger' import tar from 'tar' import { PassThrough as PassThroughStream } from 'stream' +import type { PackageJson } from '@npm/types' type TagType = 'prerelease' | 'latest' -export default class NpmPublish extends Task { - static description = '' +const semVerRegex = /^v\d+\.\d+\.\d+(-.+)?/ +const prereleaseRegex = /^v\d+\.\d+\.\d+(?:-\w+\.\d+)$/ +const releaseRegex = /^v\d+\.\d+\.\d+$/ +export default class NpmPublish extends Task { getNpmTag(tag: string): TagType { if (!tag) { throw new ToolKitError( - 'CIRCLE_TAG environment variable not found. Make sure you are running this on a release version!' + 'No `tag` variable found in the Tool Kit `ci` state. Make sure this task is running on a CI tag branch.' ) } if (prereleaseRegex.test(tag)) { @@ -28,7 +30,7 @@ export default class NpmPublish extends Task { return 'latest' } throw new ToolKitError( - `CIRCLE_TAG does not match regex ${semVerRegex}. Configure your release version to match the regex eg. v1.2.3-beta.8` + `The Tool Kit \`ci\` state \`tag\` variable ${tag} does not match regex ${semVerRegex}. Configure your release version to match the regex eg. v1.2.3-beta.8` ) } @@ -37,7 +39,7 @@ export default class NpmPublish extends Task { new PassThroughStream() .end(tarball) - .pipe(tar.t({ onentry: (entry) => this.logger.info(`- ${styles.filepath(entry.header.path)}`) })) + .pipe(tar.t({ onentry: (entry) => this.logger.info(`- ${styles.filepath(entry.path)}`) })) } async run(): Promise { @@ -73,7 +75,11 @@ export default class NpmPublish extends Task { await this.listPackedFiles(tarball) - await publish(manifest, tarball, { + // HACK:KB:20231127 cast the manifest to a PackageJson. libnpmpublish expects a + // PackageJson, but pacote.ManifestResult isn't assignable to that, because the + // definition of PackageJson in @npm/types is incorrect lol + // https://github.com/npm/types/pull/18 + await publish(manifest as PackageJson, tarball, { access: 'public', defaultTag: npmTag, forceAuth: { diff --git a/plugins/npm/test/index.test.ts b/plugins/npm/test/index.test.ts deleted file mode 100644 index a238b7e35..000000000 --- a/plugins/npm/test/index.test.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { describe, it, expect } from '@jest/globals' -import * as npm from '../' - -describe('npm plugin', () => { - it('should define package.json hooks', () => { - expect(npm.hooks).toEqual( - expect.objectContaining({ - 'build:local': expect.any(Function), - 'test:local': expect.any(Function), - 'run:local': expect.any(Function) - }) - ) - }) -}) diff --git a/plugins/npm/test/npm-publish.test.ts b/plugins/npm/test/npm-publish.test.ts index b4950a052..ac65fc8ef 100644 --- a/plugins/npm/test/npm-publish.test.ts +++ b/plugins/npm/test/npm-publish.test.ts @@ -1,14 +1,12 @@ -import { semVerRegex } from '@dotcom-tool-kit/types/lib/npm' -import NpmPublish from '../src/tasks/npm-publish' +import NpmPublish from '../src/tasks/publish' import winston, { Logger } from 'winston' -import { ToolKitError } from '../../../lib/error/lib' import * as state from '@dotcom-tool-kit/state' import pacote, { ManifestResult } from 'pacote' import { publish } from 'libnpmpublish' import pack from 'libnpmpack' import { writeFile } from 'fs/promises' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger const readStateMock = jest.spyOn(state, 'readState') jest.spyOn(pacote, 'manifest').mockImplementation(() => Promise.resolve({} as ManifestResult)) @@ -35,47 +33,43 @@ describe('NpmPublish', () => { it('should throw an error if ci is not found in state', async () => { readStateMock.mockReturnValue(null) - const task = new NpmPublish(logger, {}) + const task = new NpmPublish(logger, 'NpmPublish', {}) await expect(async () => { await task.run() - }).rejects.toThrow( - new ToolKitError(`Could not find state for ci, check that you are running this task on circleci`) + }).rejects.toThrowErrorMatchingInlineSnapshot( + `"Could not find state for ci, check that you are running this task on circleci"` ) }) it('should throw error if tag is not found', async () => { readStateMock.mockReturnValue({ tag: '', repo: '', branch: '', version: '' }) - const task = new NpmPublish(logger, {}) + const task = new NpmPublish(logger, 'NpmPublish', {}) await expect(async () => { await task.run() - }).rejects.toThrow( - new ToolKitError( - 'CIRCLE_TAG environment variable not found. Make sure you are running this on a release version!' - ) + }).rejects.toThrowErrorMatchingInlineSnapshot( + `"No \`tag\` variable found in the Tool Kit \`ci\` state. Make sure this task is running on a CI tag branch."` ) }) it('should return prerelease if match prerelease regex in getNpmTag', () => { - const task = new NpmPublish(logger, {}) + const task = new NpmPublish(logger, 'NpmPublish', {}) expect(task.getNpmTag('v1.6.0-beta.1')).toEqual('prerelease') }) it('should return latest if match latest regex in getNpmTag', () => { - const task = new NpmPublish(logger, {}) + const task = new NpmPublish(logger, 'NpmPublish', {}) expect(task.getNpmTag('v1.6.0')).toEqual('latest') }) it('should throw error if tag does not match semver regex', async () => { readStateMock.mockReturnValue({ tag: 'random-branch', repo: '', branch: '', version: '' }) - const task = new NpmPublish(logger, {}) + const task = new NpmPublish(logger, 'NpmPublish', {}) await expect(async () => { await task.run() - }).rejects.toThrow( - new ToolKitError( - `CIRCLE_TAG does not match regex ${semVerRegex}. Configure your release version to match the regex eg. v1.2.3-beta.8` - ) + }).rejects.toThrowErrorMatchingInlineSnapshot( + `"The Tool Kit \`ci\` state \`tag\` variable random-branch does not match regex /^v\\d+\\.\\d+\\.\\d+(-.+)?/. Configure your release version to match the regex eg. v1.2.3-beta.8"` ) }) @@ -85,7 +79,7 @@ describe('NpmPublish', () => { const listPackedFilesSpy = jest.spyOn(NpmPublish.prototype, 'listPackedFiles') listPackedFilesSpy.mockImplementation(() => Promise.resolve()) - const task = new NpmPublish(logger, {}) + const task = new NpmPublish(logger, 'NpmPublish', {}) await task.run() expect(listPackedFilesSpy).toBeCalled() @@ -98,7 +92,7 @@ describe('NpmPublish', () => { process.env.NPM_AUTH_TOKEN = process.env.NPM_AUTH_TOKEN || 'dummy_value' readStateMock.mockReturnValue({ tag: MOCK_CIRCLE_TAG, repo: '', branch: '', version: '' }) - const task = new NpmPublish(logger, {}) + const task = new NpmPublish(logger, 'NpmPublish', {}) await task.run() expect(writeFile).toHaveBeenCalledWith( diff --git a/plugins/npm/tsconfig.json b/plugins/npm/tsconfig.json index 8a5c37f81..2a922e8f9 100644 --- a/plugins/npm/tsconfig.json +++ b/plugins/npm/tsconfig.json @@ -9,14 +9,14 @@ "path": "../../lib/error" }, { - "path": "../../lib/package-json-hook" + "path": "../../plugins/package-json-hook" }, { - "path": "../../lib/types" + "path": "../../lib/base" }, { "path": "../../lib/state" - }, + } ], "include": [ "src/**/*" diff --git a/plugins/pa11y/.toolkitrc.yml b/plugins/pa11y/.toolkitrc.yml index 7563f8d01..5abe58fe5 100644 --- a/plugins/pa11y/.toolkitrc.yml +++ b/plugins/pa11y/.toolkitrc.yml @@ -1,5 +1,10 @@ plugins: - '@dotcom-tool-kit/doppler' -hooks: +tasks: + Pa11y: './lib/tasks/pa11y' + +commands: 'test:local': Pa11y + +version: 2 diff --git a/plugins/pa11y/jest.config.js b/plugins/pa11y/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/pa11y/jest.config.js +++ b/plugins/pa11y/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/pa11y/package.json b/plugins/pa11y/package.json index 63834219d..1ff671446 100644 --- a/plugins/pa11y/package.json +++ b/plugins/pa11y/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/pa11y", - "version": "0.5.2", + "version": "1.0.0-beta.5", "description": "pa11y", "main": "lib", "scripts": { @@ -12,7 +12,7 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", "pa11y-ci": "^3.0.1", "tslib": "^2.3.1" }, @@ -28,13 +28,13 @@ ".toolkitrc.yml" ], "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "devDependencies": { "@types/pa11y": "^5.3.4" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/pa11y/readme.md b/plugins/pa11y/readme.md index 4e278410c..9741615c5 100644 --- a/plugins/pa11y/readme.md +++ b/plugins/pa11y/readme.md @@ -23,17 +23,20 @@ plugins: - '@dotcom-tool-kit/pa11y' ``` -## Options + +## Tasks -| Key | Description | Default value | -|-|-|-| -| `configFile` | Path to the config file | `.pa11yci.js` | +### `Pa11y` -## Tasks +runs `pa11y-ci` to execute Pa11y tests +#### Task options + +| Property | Description | Type | +| :----------- | :---------------------- | :------- | +| `configFile` | Path to the config file | `string` | -| Task | Description | Preconfigured hook | -|-|-|-| -| `Pa11y` | runs `pa11y-ci` to execute Pa11y tests | `test:local` | +_All properties are optional._ + ## To note diff --git a/plugins/pa11y/src/index.ts b/plugins/pa11y/src/index.ts deleted file mode 100644 index 7e068ccc6..000000000 --- a/plugins/pa11y/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import Pa11y from './tasks/pa11y' - -export const tasks = [Pa11y] diff --git a/plugins/pa11y/src/tasks/pa11y.ts b/plugins/pa11y/src/tasks/pa11y.ts index 7b627366b..1b13e81a9 100644 --- a/plugins/pa11y/src/tasks/pa11y.ts +++ b/plugins/pa11y/src/tasks/pa11y.ts @@ -1,14 +1,12 @@ import { hookFork, waitOnExit } from '@dotcom-tool-kit/logger' -import { Task } from '@dotcom-tool-kit/types' -import type { Pa11ySchema } from '@dotcom-tool-kit/types/lib/schema/pa11y' +import { Task } from '@dotcom-tool-kit/base' +import type { Pa11ySchema } from '@dotcom-tool-kit/schemas/lib/tasks/pa11y' import { fork } from 'child_process' import { readState } from '@dotcom-tool-kit/state' const pa11yCIPath = require.resolve('pa11y-ci/bin/pa11y-ci') -export default class Pa11y extends Task { - static description = '' - +export default class Pa11y extends Task<{ task: typeof Pa11ySchema }> { async run(): Promise { const localState = readState('local') const reviewState = readState('review') diff --git a/plugins/pa11y/test/pa11y.test.ts b/plugins/pa11y/test/pa11y.test.ts index 77d9a2bdc..9b9c8d5dd 100644 --- a/plugins/pa11y/test/pa11y.test.ts +++ b/plugins/pa11y/test/pa11y.test.ts @@ -5,7 +5,7 @@ import EventEmitter from 'events' import * as state from '@dotcom-tool-kit/state' const appName = 'test-app-name' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger jest.mock('child_process', () => ({ fork: jest.fn(() => { @@ -35,14 +35,14 @@ describe('pa11y', () => { }) it("sets process.env.TEST_URL as a herokuapp url if readState('review') is truthy", async () => { MOCK_ENV = 'ci' - const pa11y = new Pa11y(logger, {}) + const pa11y = new Pa11y(logger, 'Pa11y', {}, {}) await pa11y.run() expect(process.env.TEST_URL).toBe(`https://${appName}.herokuapp.com`) }) it("sets process.env.TEST_URL as a local env url if readState('local') is truthy", async () => { MOCK_ENV = 'local' - const pa11y = new Pa11y(logger, {}) + const pa11y = new Pa11y(logger, 'Pa11y', {}, {}) await pa11y.run() expect(process.env.TEST_URL).toBe(`https://local.ft.com:5050`) diff --git a/plugins/pa11y/tsconfig.json b/plugins/pa11y/tsconfig.json index a2b756faf..6f290624f 100644 --- a/plugins/pa11y/tsconfig.json +++ b/plugins/pa11y/tsconfig.json @@ -3,18 +3,26 @@ "compilerOptions": { "outDir": "lib", "rootDir": "src", - "lib": ["ES2019", "DOM"] + "lib": [ + "ES2019", + "DOM" + ] }, "references": [ { - "path": "../../lib/types" + "path": "../../lib/base" }, { "path": "../../lib/logger" }, { "path": "../../lib/error" + }, + { + "path": "../../lib/schemas" } ], - "include": ["src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/plugins/package-json-hook/.toolkitrc.yml b/plugins/package-json-hook/.toolkitrc.yml new file mode 100644 index 000000000..427f0b70c --- /dev/null +++ b/plugins/package-json-hook/.toolkitrc.yml @@ -0,0 +1,7 @@ +installs: + PackageJson: + entryPoint: './lib/package-json-helper' + managesFiles: + - 'package.json' + +version: 2 diff --git a/lib/package-json-hook/CHANGELOG.md b/plugins/package-json-hook/CHANGELOG.md similarity index 100% rename from lib/package-json-hook/CHANGELOG.md rename to plugins/package-json-hook/CHANGELOG.md diff --git a/lib/package-json-hook/jest.config.js b/plugins/package-json-hook/jest.config.js similarity index 80% rename from lib/package-json-hook/jest.config.js rename to plugins/package-json-hook/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/lib/package-json-hook/jest.config.js +++ b/plugins/package-json-hook/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/lib/package-json-hook/package.json b/plugins/package-json-hook/package.json similarity index 62% rename from lib/package-json-hook/package.json rename to plugins/package-json-hook/package.json index df87115c2..532745202 100644 --- a/lib/package-json-hook/package.json +++ b/plugins/package-json-hook/package.json @@ -1,15 +1,18 @@ { "name": "@dotcom-tool-kit/package-json-hook", - "version": "4.2.0", + "version": "5.0.0-beta.2", "description": "", "main": "lib", "scripts": { - "test": "cd ../../ ; npx jest --silent --projects lib/package-json-hook" + "test": "cd ../../ ; npx jest --silent --projects plugins/package-json-hook" }, "keywords": [], "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/conflict": "2.0.0-beta.0", + "@dotcom-tool-kit/plugin": "2.0.0-beta.0", "@financial-times/package-json": "^3.0.0", "lodash": "^4.17.21", "tslib": "^2.3.1" @@ -17,14 +20,19 @@ "repository": { "type": "git", "url": "https://github.com/financial-times/dotcom-tool-kit.git", - "directory": "lib/package-json-hook" + "directory": "plugins/package-json-hook" }, "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", - "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/lib/package-json-hook", + "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/package-json-hook", + "peerDependencies": { + "zod": "^3.22.4" + }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "@types/lodash": "^4.14.185", - "winston": "^3.5.1" + "winston": "^3.5.1", + "zod": "^3.22.4" }, "files": [ "/lib", @@ -34,7 +42,7 @@ "extends": "../../package.json" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/package-json-hook/readme.md b/plugins/package-json-hook/readme.md new file mode 100644 index 000000000..42c4a619e --- /dev/null +++ b/plugins/package-json-hook/readme.md @@ -0,0 +1,70 @@ +# @dotcom-tool-kit/package-json-hook + +This plugin manages Tool Kit commands that are run from npm scripts, via a Tool Kit [`Hook`](#hooks) that automatically manages `package.json`. + +Although you can write npm scripts yourself in `package.json`, this plugin allows other Tool Kit plugins to define them in a repeatable, modular way. Adding custom scripts via configuring this plugin in your `.toolkitrc.yml` means that if a new use case emerges, it's much easier to abstract it into a plugin that can be shared with other Tool Kit users. + +## Installation + +Most repositories won't need to use this plugin directly. It's installed of a dependency of plugins like [`npm`](../npm) which configure it with standard use cases. + + +Install `@dotcom-tool-kit/package-json-hook` as a `devDependency` in your app: + +```sh +npm install --save-dev @dotcom-tool-kit/package-json-hook +``` + +Add the plugin to your [Tool Kit configuration](https://github.com/financial-times/dotcom-tool-kit/blob/main/readme.md#configuration): + +```yaml +plugins: + - '@dotcom-tool-kit/package-json-hook' +``` + +And install this plugin's hooks: + +```sh +npx dotcom-tool-kit --install +``` + + +## Hooks + +### `PackageJson` + +This hook accepts a nested object with a structure that matches the generated output in `package.json`. The values are used as Tool Kit command names to run. You can provide a single command or an array; multiple commands are concatenated in order. + +For more complex use cases, you can provide an object instead of a command. The object must contain keys `commands` (as above), and `trailingString` (which will be appended to the resulting `dotcom-tool-kit` CLI invocation). This is useful for tasks that accept a list of files after a trailing `--`. + +Options provided in your repository's `.toolkitrc.yml` for this hook are merged with any Tool Kit plugin that also provides options for the hook. + +For example, configuring this hook with the following options: + +~~~yml +options: + hooks: + - PackageJson: + scripts: + start: 'run:local' + customScript: + commands: + - custom:one + - custom:two + trailingString: '--' +~~~ + +will result in the following output in `package.json`: + +~~~json +{ + "scripts": { + "start": "dotcom-tool-kit run:local", + "customScript": "dotcom-tool-kit custom:one custom:two --" + } +} +~~~ + + + + diff --git a/plugins/package-json-hook/src/package-json-helper.ts b/plugins/package-json-hook/src/package-json-helper.ts new file mode 100644 index 000000000..73f1829ae --- /dev/null +++ b/plugins/package-json-hook/src/package-json-helper.ts @@ -0,0 +1,248 @@ +import type { z } from 'zod' +import { Hook, HookInstallation } from '@dotcom-tool-kit/base' +import type { Plugin } from '@dotcom-tool-kit/plugin' +import fs from 'fs' +import get from 'lodash/get' +import set from 'lodash/set' +import partition from 'lodash/partition' +import update from 'lodash/update' +import merge from 'lodash/merge' +import path from 'path' + +import { PackageJsonSchema } from '@dotcom-tool-kit/schemas/lib/hooks/package-json' +import { Conflict, isConflict } from '@dotcom-tool-kit/conflict' + +interface PackageJsonContents { + [field: string]: PackageJsonContents | string +} + +interface PackageJsonStateValue { + commands: string[] + trailingString?: string + installedBy: PackageJson +} + +interface PackageJsonState { + [path: string]: PackageJsonStateValue +} + +function installationsOverlap( + installation: HookInstallation>, + other: HookInstallation> +): boolean { + for (const [field, object] of Object.entries(installation.options)) { + for (const key of Object.keys(object)) { + if (field in other.options && key in other.options[field]) { + return true + } + } + } + + return false +} + +function partitionInstallations( + installation: HookInstallation>, + mergeable: HookInstallation>[], + unmergeable: HookInstallation>[] +): [ + HookInstallation>[], + HookInstallation>[] +] { + const [noLongerMergeable, stillMergeable] = partition(mergeable, (other) => + installationsOverlap(installation, other) + ) + + const overlapsWithUnmergeable = unmergeable.some((other) => installationsOverlap(installation, other)) + + if (noLongerMergeable.length > 0 || overlapsWithUnmergeable) { + return [stillMergeable, [...unmergeable, ...noLongerMergeable, installation]] + } + + return [[...stillMergeable, installation], unmergeable] +} + +function mergeInstallationResults( + plugin: Plugin, + mergeable: HookInstallation>[], + unmergeable: HookInstallation>[] +) { + const results: (HookInstallation> | Conflict)[] = [] + + if (mergeable.length > 0) { + results.push({ + plugin, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: merge({}, ...mergeable.map((installation) => installation.options)) + }) + } + + if (unmergeable.length > 0) { + results.push({ + plugin, + conflicting: unmergeable + }) + } + + return results +} + +// split the path on all unescaped full stops, then unescape all escaped full +// stops. this will mean we can pass fields to lodash functions taking a path +// even if the fields contain full stops. +function splitAndUnescapePath(path: string): string[] { + return path.split(/(? component.replace('\\.', '.')) +} + +export default class PackageJson extends Hook { + private _packageJson?: PackageJsonContents + + installGroup = 'package-json' + + filepath = path.resolve(process.cwd(), 'package.json') + + static mergeChildInstallations( + plugin: Plugin, + childInstallations: ( + | HookInstallation> + | Conflict>> + )[] + ): (HookInstallation> | Conflict)[] { + let mergeable: HookInstallation>[] = [] + let unmergeable: HookInstallation>[] = [] + + for (const installation of childInstallations) { + if (isConflict(installation)) { + unmergeable.push(...installation.conflicting) + } else { + ;[mergeable, unmergeable] = partitionInstallations(installation, mergeable, unmergeable) + } + } + + return mergeInstallationResults(plugin, mergeable, unmergeable) + } + + static overrideChildInstallations( + plugin: Plugin, + parentInstallation: HookInstallation>, + childInstallations: ( + | HookInstallation> + | Conflict>> + )[] + ): (HookInstallation> | Conflict)[] { + const mergeable: HookInstallation>[] = [] + const unmergeable: HookInstallation>[] = [] + + for (const installation of childInstallations) { + if (isConflict(installation)) { + const [canHandle, cannotHandle] = partition(installation.conflicting, (other) => + installationsOverlap(parentInstallation, other) + ) + + mergeable.push(...canHandle) + unmergeable.push(...cannotHandle) + } else { + mergeable.push(installation) + } + } + + mergeable.push(parentInstallation) + + return mergeInstallationResults(plugin, mergeable, unmergeable) + } + + async getPackageJson(): Promise { + if (!this._packageJson) { + const rawPackageJson = await fs.promises.readFile(this.filepath, 'utf8') + const packageJson = JSON.parse(rawPackageJson) + this._packageJson = packageJson + return packageJson + } + + return this._packageJson + } + + async isInstalled(): Promise { + const packageJson = await this.getPackageJson() + + // this instance's `options` is a nested object of expected package.json field/command mappings, e.g. + // { "scripts": { "build": "build:local" } }. in the package.json, they'll have the same structure + // with a `dotcom-tool-kit` CLI prefix, e.g. { "scripts": { "build": "dotcom-tool-kit build:local" } }. + // loop through the nested options object, get the same nested key from package.json, and check that + // field exists, and its string includes the name of the command. if any command from our options is + // missing, the check should fail. + for (const [field, object] of Object.entries(this.options)) { + for (const [key, entry] of Object.entries(object)) { + let commands: string[] + if (Array.isArray(entry)) { + commands = entry + } else if (typeof entry === 'string') { + commands = [entry] + } else { + commands = Array.isArray(entry.commands) ? entry.commands : [entry.commands] + } + + const path = [...splitAndUnescapePath(field), key] + const currentPackageJsonField: string = get(packageJson, path) + + if ( + !currentPackageJsonField || + !commands.every((command) => currentPackageJsonField.includes(command)) + ) { + return false + } + } + } + + return true + } + + async install(state: PackageJsonState = {}): Promise { + for (const [field, object] of Object.entries(this.options)) { + for (const [key, entry] of Object.entries(object)) { + let trailingString: string | undefined + let commands: string[] + + if (Array.isArray(entry)) { + commands = entry + } else if (typeof entry === 'string') { + commands = [entry] + } else { + commands = Array.isArray(entry.commands) ? entry.commands : [entry.commands] + trailingString = entry.trailingString + } + + update( + state, + // full stops in the key shouldn't be treated as path separators + [field + '.' + key.replace('.', '\\.')], + (hookState?: PackageJsonStateValue): PackageJsonStateValue => ({ + // prepend each command to maintain the same order as previous implementations + commands: [...commands, ...(hookState?.commands ?? [])], + installedBy: this, + trailingString: trailingString + }) + ) + } + } + + return state + } + + async commitInstall(state: PackageJsonState): Promise { + const packageJson = await this.getPackageJson() + + for (const [path, installation] of Object.entries(state)) { + set( + packageJson, + splitAndUnescapePath(path), + `dotcom-tool-kit ${installation.commands.join(' ')}${ + installation.trailingString ? ' ' + installation.trailingString : '' + }` + ) + } + + await fs.promises.writeFile(this.filepath, JSON.stringify(packageJson, null, 2) + '\n') + } +} diff --git a/lib/package-json-hook/test/files/existing-hook/package.json b/plugins/package-json-hook/test/files/existing-hook/package.json similarity index 100% rename from lib/package-json-hook/test/files/existing-hook/package.json rename to plugins/package-json-hook/test/files/existing-hook/package.json diff --git a/lib/package-json-hook/test/files/multiple-hooks/package.json b/plugins/package-json-hook/test/files/multiple-hooks/package.json similarity index 100% rename from lib/package-json-hook/test/files/multiple-hooks/package.json rename to plugins/package-json-hook/test/files/multiple-hooks/package.json diff --git a/lib/package-json-hook/test/files/with-hook/package.json b/plugins/package-json-hook/test/files/with-hook/package.json similarity index 100% rename from lib/package-json-hook/test/files/with-hook/package.json rename to plugins/package-json-hook/test/files/with-hook/package.json diff --git a/lib/package-json-hook/test/files/without-hook/package.json b/plugins/package-json-hook/test/files/without-hook/package.json similarity index 100% rename from lib/package-json-hook/test/files/without-hook/package.json rename to plugins/package-json-hook/test/files/without-hook/package.json diff --git a/plugins/package-json-hook/test/index.test.ts b/plugins/package-json-hook/test/index.test.ts new file mode 100644 index 000000000..82d0644c6 --- /dev/null +++ b/plugins/package-json-hook/test/index.test.ts @@ -0,0 +1,643 @@ +import { describe, it, expect } from '@jest/globals' +import * as path from 'path' +import { promises as fs } from 'fs' +import PackageJson from '../src/package-json-helper' +import winston, { Logger } from 'winston' +import { HookInstallation } from '@dotcom-tool-kit/base' +import { PackageJsonSchema } from '@dotcom-tool-kit/schemas/lib/hooks/package-json' + +const logger = winston as unknown as Logger + +describe('package.json hook', () => { + const originalDir = process.cwd() + + afterEach(() => { + process.chdir(originalDir) + }) + + describe('check', () => { + it('should return true when package.json has hook call in script', async () => { + process.chdir(path.join(__dirname, 'files', 'with-hook')) + const hook = new PackageJson(logger, 'PackageJson', { + scripts: { + 'test-hook': 'test:hook' + } + }) + + expect(await hook.isInstalled()).toBeTruthy() + }) + + it('should return true when script includes other hooks', async () => { + process.chdir(path.join(__dirname, 'files', 'multiple-hooks')) + const hook = new PackageJson(logger, 'PackageJson', { + scripts: { + 'test-hook': 'test:hook' + } + }) + + expect(await hook.isInstalled()).toBeTruthy() + }) + + it(`should return false when package.json doesn't have hook call in script`, async () => { + process.chdir(path.join(__dirname, 'files', 'without-hook')) + const hook = new PackageJson(logger, 'PackageJson', { + scripts: { + 'test-hook': 'test:hook' + } + }) + + expect(await hook.isInstalled()).toBeFalsy() + }) + }) + + describe('install', () => { + it(`should add script when it doesn't exist`, async () => { + const base = path.join(__dirname, 'files', 'without-hook') + + const pkgPath = path.join(base, 'package.json') + const originalJson = await fs.readFile(pkgPath, 'utf-8') + + process.chdir(base) + + try { + const hook = new PackageJson(logger, 'PackageJson', { + scripts: { + 'test-hook': 'test:hook' + } + }) + const state = await hook.install() + await hook.commitInstall(state) + + const packageJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8')) + + expect(packageJson).toMatchInlineSnapshot(` + { + "scripts": { + "test-hook": "dotcom-tool-kit test:hook", + }, + } + `) + } finally { + await fs.writeFile(pkgPath, originalJson) + } + }) + + it(`should append trailingString field`, async () => { + const base = path.join(__dirname, 'files', 'without-hook') + + const pkgPath = path.join(base, 'package.json') + const originalJson = await fs.readFile(pkgPath, 'utf-8') + + process.chdir(base) + + try { + const hook = new PackageJson(logger, 'PackageJson', { + scripts: { + 'test-hook': { + trailingString: '--', + commands: 'test:hook' + } + } + }) + const state = await hook.install() + await hook.commitInstall(state) + + const packageJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8')) + + expect(packageJson).toMatchInlineSnapshot(` + { + "scripts": { + "test-hook": "dotcom-tool-kit test:hook --", + }, + } + `) + } finally { + await fs.writeFile(pkgPath, originalJson) + } + }) + + it(`should allow nested field property`, async () => { + const base = path.join(__dirname, 'files', 'without-hook') + + const pkgPath = path.join(base, 'package.json') + const originalJson = await fs.readFile(pkgPath, 'utf-8') + + process.chdir(base) + + try { + const hook = new PackageJson(logger, 'PackageJson', { + 'scripts.nested': { + 'test-hook': 'test:hook' + } + }) + const state = await hook.install() + await hook.commitInstall(state) + + const packageJson = JSON.parse(await fs.readFile(pkgPath, 'utf-8')) + + expect(packageJson).toMatchInlineSnapshot(` + { + "scripts": { + "nested": { + "test-hook": "dotcom-tool-kit test:hook", + }, + }, + } + `) + } finally { + await fs.writeFile(pkgPath, originalJson) + } + }) + }) + + describe('conflict resolution', () => { + it('should merge children setting different fields', () => { + const childInstallations = [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + build: 'build:local' + } + } + }, + { + plugin: { id: 'c', root: 'plugins/c' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + another: { + field: 'something:else' + } + } + } + ] + const plugin = { id: 'p', root: 'plugins/p' } + + expect( + PackageJson.mergeChildInstallations( + plugin, + childInstallations as unknown as HookInstallation>[] + ) + ).toEqual([ + { + plugin, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local', + build: 'build:local' + }, + another: { + field: 'something:else' + } + } + } + ]) + }) + + it('should conflict sibling plugins setting the same field', () => { + const childInstallations = [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:ci' + } + } + } + ] + + const plugin = { id: 'p', root: 'plugins/p' } + + expect( + PackageJson.mergeChildInstallations( + plugin, + childInstallations as unknown as HookInstallation>[] + ) + ).toEqual([ + { + plugin, + conflicting: childInstallations + } + ]) + }) + + it('should split conflicting and non-conflicting sibling plugins', () => { + const childInstallations = [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:ci' + } + } + }, + { + plugin: { id: 'c', root: 'plugins/c' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + build: 'build:local' + } + } + }, + { + plugin: { id: 'd', root: 'plugins/d' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + start: 'run:local' + } + } + } + ] + + const plugin = { id: 'p', root: 'plugins/p' } + + expect( + PackageJson.mergeChildInstallations( + plugin, + childInstallations as unknown as HookInstallation>[] + ) + ).toEqual([ + { + plugin, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + build: 'build:local', + start: 'run:local' + } + } + }, + { + plugin, + conflicting: [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:ci' + } + } + } + ] + } + ]) + }) + + it('should merge parent and child installations, preferring parent', () => { + const plugin = { id: 'p', root: 'plugins/p' } + + const parentInstallation = { + plugin, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + } + + const childInstallations = [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:ci' + }, + another: { + field: 'something:else' + } + } + } + ] + + expect( + PackageJson.overrideChildInstallations( + plugin, + parentInstallation as unknown as HookInstallation>, + childInstallations as unknown as HookInstallation>[] + ) + ).toEqual([ + { + plugin, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + }, + another: { + field: 'something:else' + } + } + } + ]) + }) + + it(`should override conflicts that are solved by the parent`, () => { + const plugin = { id: 'p', root: 'plugins/p' } + + const parentInstallation = { + plugin, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + } + + const childInstallations = [ + { + plugin: { id: 'c', root: 'plugins/c' }, + conflicting: [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:ci' + } + } + } + ] + } + ] + + expect( + PackageJson.overrideChildInstallations( + plugin, + parentInstallation as unknown as HookInstallation>, + childInstallations as unknown as HookInstallation>[] + ) + ).toEqual([ + { + plugin, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + } + ]) + }) + + it(`should keep conflicts that aren't solved by the parent`, () => { + const plugin = { id: 'p', root: 'plugins/p' } + + const parentInstallation = { + plugin, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + } + + const childInstallations = [ + { + plugin: { id: 'c', root: 'plugins/c' }, + conflicting: [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + build: 'build:local' + } + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + build: 'build:ci' + } + } + } + ] + } + ] + + expect( + PackageJson.overrideChildInstallations( + plugin, + parentInstallation as unknown as HookInstallation>, + childInstallations as unknown as HookInstallation>[] + ) + ).toEqual([ + { + plugin, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + }, + { + plugin: { id: 'p', root: 'plugins/p' }, + conflicting: childInstallations[0].conflicting + } + ]) + }) + + it(`should partially override only the conflicts solvable by the parent`, () => { + const plugin = { id: 'p', root: 'plugins/p' } + + const parentInstallation = { + plugin, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + } + + const childInstallations = [ + { + plugin: { id: 'd', root: 'plugins/d' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + another: { + field: 'something:else' + } + } + }, + { + plugin: { id: 'c', root: 'plugins/c' }, + conflicting: [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + build: 'build:local' + } + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + build: 'build:ci' + } + } + }, + { + plugin: { id: 'e', root: 'plugins/e' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + } + } + }, + { + plugin: { id: 'f', root: 'plugins/f' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:ci' + } + } + } + ] + } + ] + + expect( + PackageJson.overrideChildInstallations( + plugin, + parentInstallation as unknown as HookInstallation>, + childInstallations as unknown as HookInstallation>[] + ) + ).toEqual([ + { + plugin, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + test: 'test:local' + }, + another: { + field: 'something:else' + } + } + }, + { + plugin: { id: 'p', root: 'plugins/p' }, + conflicting: [ + { + plugin: { id: 'a', root: 'plugins/a' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + build: 'build:local' + } + } + }, + { + plugin: { id: 'b', root: 'plugins/b' }, + forHook: 'PackageJson', + hookConstructor: PackageJson, + options: { + scripts: { + build: 'build:ci' + } + } + } + ] + } + ]) + }) + }) +}) diff --git a/plugins/package-json-hook/tsconfig.json b/plugins/package-json-hook/tsconfig.json new file mode 100644 index 000000000..b3b24cebb --- /dev/null +++ b/plugins/package-json-hook/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "references": [ + { + "path": "../../lib/base" + }, + { + "path": "../../lib/schemas" + }, + { + "path": "../../lib/conflict" + }, + { + "path": "../../lib/plugin" + } + ], + "include": ["src/**/*"] +} diff --git a/plugins/parallel/.toolkitrc.yml b/plugins/parallel/.toolkitrc.yml new file mode 100644 index 000000000..91982419c --- /dev/null +++ b/plugins/parallel/.toolkitrc.yml @@ -0,0 +1,4 @@ +version: 2 + +tasks: + Parallel: './lib/tasks/parallel' diff --git a/lib/types/package.json b/plugins/parallel/package.json similarity index 55% rename from lib/types/package.json rename to plugins/parallel/package.json index 95b4e99f7..9915eafb2 100644 --- a/lib/types/package.json +++ b/plugins/parallel/package.json @@ -1,6 +1,6 @@ { - "name": "@dotcom-tool-kit/types", - "version": "3.6.0", + "name": "@dotcom-tool-kit/parallel", + "version": "0.1.0", "description": "", "main": "lib", "scripts": { @@ -12,31 +12,27 @@ "repository": { "type": "git", "url": "https://github.com/financial-times/dotcom-tool-kit.git", - "directory": "lib/types" + "directory": "plugins/parallel" }, "bugs": "https://github.com/financial-times/dotcom-tool-kit/issues", - "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/lib/types", + "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/parallel", "files": [ - "/lib" + "/lib", + ".toolkitrc.yml" ], + "engines": { + "node": "18.x || 20.x", + "npm": "7.x || 8.x || 9.x" + }, "volta": { "extends": "../../package.json" }, - "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "semver": "^7.3.7", - "tslib": "^2.3.1", - "zod": "^3.20.2" - }, - "devDependencies": { - "@jest/globals": "^27.4.6", - "@types/prompts": "^2.0.14", - "@types/semver": "^7.3.9", - "winston": "^3.5.1" + "peerDependencies": { + "dotcom-tool-kit": "4.0.0-beta.5" }, - "engines": { - "node": "16.x || 18.x || 20.x", - "npm": "7.x || 8.x || 9.x || 10.x" + "dependencies": { + "@dotcom-tool-kit/base": "^4.0.0-beta.0", + "@dotcom-tool-kit/schemas": "^2.0.0-beta.0", + "@dotcom-tool-kit/task": "^1.2.0" } } diff --git a/plugins/parallel/readme.md b/plugins/parallel/readme.md new file mode 100644 index 000000000..10412c49f --- /dev/null +++ b/plugins/parallel/readme.md @@ -0,0 +1,54 @@ +# @dotcom-tool-kit/parallel + +This plugin allows you to run Tool Kit tasks in parallel. + +## Installation + +Install `@dotcom-tool-kit/parallel` as a `devDependency` in your app: + +```sh +npm install --save-dev @dotcom-tool-kit/parallel +``` + +Add the plugin to your [Tool Kit configuration](https://github.com/financial-times/dotcom-tool-kit/blob/main/readme.md#configuration): + +```yaml +plugins: + - '@dotcom-tool-kit/parallel' +``` + +## Usage + +To run tasks in parallel, pass them in as the `tasks` option to the [`Parallel` task](#parallel). + +`Parallel` is intended for running long-running tasks alongside each other, such as running your server with the `Node` task at the same time as bundling your code with the `Webpack` task in watch mode. + +If any of the tasks throw an error, `Parallel` will throw an error, and attempt to stop the other running tasks. This ensures that either all of the tasks you expect to be running are actually running, or that Tool Kit has exited and printed an error message. + + +## Tasks + +### `Parallel` + +Run Tool Kit tasks in parallel + +#### Task options + +`tasks`: an array listing the tasks to run in parallel, and the options to run each task with. Each element in the array is an object with a single key and value; the key is the name of the task to run, and the value is the options object for that task. Other tasks' options are documented in their plugin's readme. + +##### Example + +~~~yaml +commands: + run:local: + - Parallel: + tasks: + - Node: + entry: server/index.js + - Webpack: + watch: true +~~~ + + + + diff --git a/plugins/parallel/src/tasks/parallel.ts b/plugins/parallel/src/tasks/parallel.ts new file mode 100644 index 000000000..19cf67222 --- /dev/null +++ b/plugins/parallel/src/tasks/parallel.ts @@ -0,0 +1,46 @@ +import { Task, TaskRunContext } from '@dotcom-tool-kit/base' +import { styles } from '@dotcom-tool-kit/logger' +import { ParallelSchema } from '@dotcom-tool-kit/schemas/lib/tasks/parallel' +import { loadTasks } from 'dotcom-tool-kit/lib/tasks' + +export default class Parallel extends Task<{ task: typeof ParallelSchema }> { + async run({ config, files }: TaskRunContext) { + const tasks = this.options.tasks.flatMap((entry) => + Object.entries(entry).map(([task, options]) => ({ task, options, plugin: this.plugin })) + ) + + this.logger.info(`running tasks in parallel: +${tasks + .map( + (task) => + ` - ${styles.task(task.task)} ${styles.dim( + `(with options ${styles.code(JSON.stringify(task.options))})` + )}` + ) + .join('\n')} +`) + + const taskInstances = (await loadTasks(this.logger, tasks, config)).unwrap('tasks are invalid!') + + // uses Promise.all so the first promise to reject stops this whole task. + // the Parallel task is intended for running multiple long-running processes + // simultaneously, like servers or watch-mode compilers. carrying on running + // if one has errored means you'll easily lose any error logs, and having + // only some of these tasks still running is almost certainly not what the + // user wants. + try { + await Promise.all(taskInstances.map((task) => task.run({ files, config }))) + } catch (error) { + await Promise.all( + taskInstances.map((task) => + task.stop().catch((error) => { + this.logger.warn(`error stopping ${styles.task(this.id)}: +${error.message}`) + }) + ) + ) + + throw error + } + } +} diff --git a/plugins/parallel/tsconfig.json b/plugins/parallel/tsconfig.json new file mode 100644 index 000000000..65a97dec3 --- /dev/null +++ b/plugins/parallel/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "outDir": "lib", + "rootDir": "src" + }, + "references": [ + { + "path": "../../lib/base" + }, + { + "path": "../../lib/schemas" + } + ], + "include": ["src/**/*"] +} diff --git a/plugins/prettier/.toolkitrc.yml b/plugins/prettier/.toolkitrc.yml index d56011a80..b90eefc4d 100644 --- a/plugins/prettier/.toolkitrc.yml +++ b/plugins/prettier/.toolkitrc.yml @@ -1,3 +1,17 @@ -hooks: +plugins: + - '@dotcom-tool-kit/package-json-hook' + +tasks: + Prettier: './lib/tasks/prettier' + +commands: 'format:local': Prettier 'format:staged': Prettier + +options: + hooks: + - PackageJson: + scripts: + format: 'format:local' + +version: 2 diff --git a/plugins/prettier/jest.config.js b/plugins/prettier/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/prettier/jest.config.js +++ b/plugins/prettier/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/prettier/package.json b/plugins/prettier/package.json index 291f6a295..40a7214b6 100644 --- a/plugins/prettier/package.json +++ b/plugins/prettier/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/prettier", - "version": "3.2.0", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -10,10 +10,10 @@ "author": "FT.com Platforms Team ", "license": "ISC", "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/package-json-hook": "^4.2.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", + "@dotcom-tool-kit/package-json-hook": "5.0.0-beta.2", "fast-glob": "^3.2.7", "hook-std": "^2.0.0", "prettier": "^2.2.1", @@ -31,19 +31,17 @@ ".toolkitrc.yml" ], "devDependencies": { - "@jest/globals": "^27.4.6", - "jest": "^27.4.7", - "ts-jest": "^27.1.3", + "@types/prettier": "^2.7.3", "winston": "^3.5.1" }, "volta": { "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/prettier/readme.md b/plugins/prettier/readme.md index ac7a217f8..a3a59a330 100644 --- a/plugins/prettier/readme.md +++ b/plugins/prettier/readme.md @@ -1,6 +1,6 @@ # @dotcom-tool-kit/prettier -This plugin is for adding prettier onto your apps. +Tool Kit plugin to format your source code with [Prettier](https://prettier.io). ## Installation @@ -17,20 +17,19 @@ plugins: - '@dotcom-tool-kit/prettier' ``` -And install this plugin's hooks: + +## Tasks -```sh -npx dotcom-tool-kit --install -``` - -This will modify your `package.json`. You should commit this change. +### `Prettier` -### Options +Format files with `prettier`. +#### Task options -| Key | Description | Default value | -|-|-|-| -| `files` | A required Array of strings of filepath(s) or filepath pattern(s) to be formatted | `['{,!(node_modules)/**/}*.js']` | -| `configOptions` | An optional prettier configuration object |
`{`
` singleQuote: true,`
` useTabs: true,`
` bracketSpacing: true,`
` arrowParens: 'always',`
` trailingComma: 'none'`
`}`
| -| `configFile` | An optional String that specifies the prettier configuration file (.prettierrc.json). The configuration file will be resolved starting from the location of the file being formatted, and searching up the file tree until a config file is (or isn’t) found. If the configFile is not found the prettier plugin will default to configOptions. | `configOptions` value | +| Property | Description | Type | Default | +| :----------- | :---------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------ | :------------------------- | +| `files` | glob pattern of files to run Prettier on. | `Array \| string` | `["**/*.{js,jsx,ts,tsx}"]` | +| `configFile` | path to a Prettier config file to use. Uses Prettier's built-in [config resolution](https://prettier.io/docs/en/configuration.html) by default. | `string` | | +| `ignoreFile` | path to a Prettier [ignore file](https://prettier.io/docs/en/ignore). | `string` | `'.prettierignore'` | -For more information on prettier configuration, visit the [Prettier docs](https://prettier.io/docs/en/configuration.html). +_All properties are optional._ + diff --git a/plugins/prettier/src/index.ts b/plugins/prettier/src/index.ts deleted file mode 100644 index ec8dfd00b..000000000 --- a/plugins/prettier/src/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { PackageJsonScriptHook } from '@dotcom-tool-kit/package-json-hook' -import Prettier from './tasks/prettier' - -export const tasks = [Prettier] - -class FormatLocal extends PackageJsonScriptHook { - static description = 'format prettier' - - key = 'format' - hook = `format:local` -} - -export const hooks = { - 'format:local': FormatLocal -} diff --git a/plugins/prettier/src/tasks/prettier.ts b/plugins/prettier/src/tasks/prettier.ts index f1df42d51..07293bd84 100644 --- a/plugins/prettier/src/tasks/prettier.ts +++ b/plugins/prettier/src/tasks/prettier.ts @@ -1,15 +1,13 @@ import prettier from 'prettier' -import { PrettierOptions, PrettierSchema } from '@dotcom-tool-kit/types/lib/schema/prettier' +import { PrettierOptions, PrettierSchema } from '@dotcom-tool-kit/schemas/lib/tasks/prettier' import { promises as fsp } from 'fs' import fg from 'fast-glob' -import { hookConsole, styles } from '@dotcom-tool-kit/logger' -import { Task } from '@dotcom-tool-kit/types' +import { hookConsole } from '@dotcom-tool-kit/logger' +import { Task, TaskRunContext } from '@dotcom-tool-kit/base' import { ToolKitError } from '@dotcom-tool-kit/error' -export default class Prettier extends Task { - static description = '' - - async run(files?: string[]): Promise { +export default class Prettier extends Task<{ task: typeof PrettierSchema }> { + async run({ files }: TaskRunContext): Promise { try { const filepaths = await fg(files ?? this.options.files) for (const filepath of filepaths) { @@ -41,14 +39,6 @@ export default class Prettier extends Task { throw error } } - if (!prettierConfig && options.configOptions) { - this.logger.warn( - `prettier could not find the specified configFile${ - options.configFile ? ` (${styles.filepath(options.configFile)})` : '' - }), using ${styles.option('configOptions')} instead` - ) - prettierConfig = options.configOptions - } const { ignored } = await prettier.getFileInfo(filepath, { ignorePath: this.options.ignoreFile }) @@ -58,10 +48,7 @@ export default class Prettier extends Task { const unhook = hookConsole(this.logger, 'prettier') try { - await fsp.writeFile( - filepath, - prettier.format(fileContent, { ...(prettierConfig as prettier.Options), filepath }) - ) + await fsp.writeFile(filepath, prettier.format(fileContent, { ...prettierConfig, filepath })) } finally { unhook() } diff --git a/plugins/prettier/test/files/fixtures/formatted-config-options.ts b/plugins/prettier/test/files/fixtures/formatted-config-options.ts deleted file mode 100644 index 5dc54f5aa..000000000 --- a/plugins/prettier/test/files/fixtures/formatted-config-options.ts +++ /dev/null @@ -1,13 +0,0 @@ -console.log({ - one: "one", - two: "two", - three: { - four: "four", - five: {}, - }, - six: "six", -}); - -console.log({ - hello: ["one", "two", "three"], -}); diff --git a/plugins/prettier/test/files/fixtures/formatted.ts b/plugins/prettier/test/files/fixtures/formatted.ts deleted file mode 100644 index aaeb339d6..000000000 --- a/plugins/prettier/test/files/fixtures/formatted.ts +++ /dev/null @@ -1,13 +0,0 @@ -console.log({ - one: 'one', - two: 'two', - three: { - four: 'four', - five: {} - }, - six: 'six' -}); - -console.log({ - hello: ['one', 'two', 'three'] -}); diff --git a/plugins/prettier/test/tasks/prettier.test.ts b/plugins/prettier/test/tasks/prettier.test.ts index 85d3aff0b..809183d91 100644 --- a/plugins/prettier/test/tasks/prettier.test.ts +++ b/plugins/prettier/test/tasks/prettier.test.ts @@ -1,26 +1,16 @@ -import { describe, it, expect, beforeAll } from '@jest/globals' import * as path from 'path' import Prettier from '../../src/tasks/prettier' import { promises as fsp } from 'fs' import winston, { Logger } from 'winston' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger const testDirectory = path.join(__dirname, '../files') -const defaultConfig = { - singleQuote: true, - useTabs: true, - bracketSpacing: true, - arrowParens: 'always', - trailingComma: 'none' -} - describe('prettier', () => { let unformattedFixture: string let formattedDefaultFixture: string let formattedConfigFileFixture: string - let formattedConfigOptionsFixture: string beforeAll(async () => { unformattedFixture = await fsp.readFile(path.join(testDirectory, './fixtures/unformatted.ts'), 'utf8') @@ -32,56 +22,42 @@ describe('prettier', () => { path.join(testDirectory, './fixtures/formatted-config-file.ts'), 'utf8' ) - formattedConfigOptionsFixture = await fsp.readFile( - path.join(testDirectory, './fixtures/formatted-config-options.ts'), - 'utf8' - ) }) beforeEach(async () => { await fsp.writeFile(path.join(testDirectory, 'unformatted.ts'), unformattedFixture) }) - it('should format the correct file with default configOptions', async () => { - const task = new Prettier(logger, { - files: [path.join(testDirectory, 'unformatted.ts')], - ignoreFile: 'nonexistent prettierignore', - configOptions: defaultConfig - }) - await task.run() + it('should format the file with default options', async () => { + const task = new Prettier( + logger, + 'Prettier', + {}, + { + files: [path.join(testDirectory, 'unformatted.ts')], + ignoreFile: 'nonexistent prettierignore' + } + ) + await task.run({ command: 'format:local' }) const prettified = await fsp.readFile(path.join(testDirectory, 'unformatted.ts'), 'utf8') expect(prettified).toEqual(formattedDefaultFixture) }) it('should use configFile if present', async () => { // having the configuration file be named .prettierrc-test.json hides it from being found by prettier on other non-test occasions. - const task = new Prettier(logger, { - files: [path.join(testDirectory, 'unformatted.ts')], - configFile: path.join(__dirname, '../.prettierrc-test.json'), - ignoreFile: 'nonexistent prettierignore', - configOptions: defaultConfig - }) - await task.run() - const prettified = await fsp.readFile(path.join(testDirectory, 'unformatted.ts'), 'utf8') - expect(prettified).toEqual(formattedConfigFileFixture) - }) - - it('should use configOptions if configFile not found', async () => { - const task = new Prettier(logger, { - files: [path.join(testDirectory, 'unformatted.ts')], - configFile: '/incorrect/.prettierrc.js', - ignoreFile: 'nonexistent prettierignore', - configOptions: { - singleQuote: false, - useTabs: true, - bracketSpacing: false, - arrowParens: 'always', - trailingComma: 'all', - semi: true + const task = new Prettier( + logger, + 'Prettier', + {}, + { + files: [path.join(testDirectory, 'unformatted.ts')], + configFile: path.join(__dirname, '../.prettierrc-test.json'), + ignoreFile: 'nonexistent prettierignore' } - }) - await task.run() + ) + + await task.run({ command: 'format:local' }) const prettified = await fsp.readFile(path.join(testDirectory, 'unformatted.ts'), 'utf8') - expect(prettified).toEqual(formattedConfigOptionsFixture) + expect(prettified).toEqual(formattedConfigFileFixture) }) }) diff --git a/plugins/prettier/tsconfig.json b/plugins/prettier/tsconfig.json index 3f2a5796a..47e6db56d 100644 --- a/plugins/prettier/tsconfig.json +++ b/plugins/prettier/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.settings.json", - "include": ["src/**/*"], + "include": [ + "src/**/*" + ], "compilerOptions": { "outDir": "lib", "rootDir": "src" @@ -13,7 +15,10 @@ "path": "../../lib/logger" }, { - "path": "../../lib/types" + "path": "../../lib/schemas" + }, + { + "path": "../../lib/base" } ] } diff --git a/plugins/serverless/.toolkitrc.yml b/plugins/serverless/.toolkitrc.yml index e69de29bb..76dd06300 100644 --- a/plugins/serverless/.toolkitrc.yml +++ b/plugins/serverless/.toolkitrc.yml @@ -0,0 +1,7 @@ +tasks: + ServerlessRun: './lib/tasks/run' + ServerlessDeploy: './lib/tasks/deploy' + ServerlessProvision: './lib/tasks/provision' + ServerlessTeardown: './lib/tasks/teardown' + +version: 2 diff --git a/plugins/serverless/package.json b/plugins/serverless/package.json index 2992b9e6b..abbc6833d 100644 --- a/plugins/serverless/package.json +++ b/plugins/serverless/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/serverless", - "version": "2.4.4", + "version": "3.0.0-beta.5", "description": "a plugin to manage and deploy apps using AWS Serverless", "main": "lib", "scripts": { @@ -21,21 +21,24 @@ ".toolkitrc.yml" ], "peerDependencies": { - "dotcom-tool-kit": "3.x", + "dotcom-tool-kit": "4.0.0-beta.5", "serverless-offline": "^12.0.4" }, "dependencies": { - "@dotcom-tool-kit/doppler": "^1.1.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/options": "^3.2.0", - "@dotcom-tool-kit/state": "^3.3.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/doppler": "2.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/options": "4.0.0-beta.0", + "@dotcom-tool-kit/state": "4.0.0-beta.0", "get-port": "^5.1.1", "tslib": "^2.3.1", "wait-port": "^0.2.9" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" + }, + "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0" } } diff --git a/plugins/serverless/readme.md b/plugins/serverless/readme.md index 7456ed5f9..c7617e53f 100644 --- a/plugins/serverless/readme.md +++ b/plugins/serverless/readme.md @@ -19,23 +19,44 @@ plugins: - '@dotcom-tool-kit/serverless' ``` -## Options - -| Key | Description | Default value | -|-|-|-| -| `awsAccountId` | [required] the ID of the AWS account you wish to deploy to (account IDs can be found at the [FT login page](https://awslogin.in.ft.com/)) | none | -| `systemCode` | [required] the system code for your app | none | -| `regions` | [optional] an array of AWS regions you want to deploy to | `['eu-west-1']` | -| `configPath` | [optional] path to your serverless config file. If this is not provided aws defaults to `./serverless.yml` but [other config fomats are accepted](https://www.serverless.com/framework/docs/providers/aws/guide/intro#alternative-configuration-format)| | -| `useVault` | option to run the application with environment variables from Vault | `true` | -| `ports` | ports to try to bind to for this application | `[3001, 3002, 3003]` | -| `buildNumVariable` | an environment variable used to get a unique ID to use in the provisioning stage | `CIRCLE_BUILD_NUM` | - + ## Tasks -| Task | Description | Default hooks | -|-|-|-| -| `ServerlessRun` | Run application with `serverless` | `run:local` | -| `ServerlessProvision` | Deploy review app with `serverless` | `deploy:review` | -| `ServerlessTeardown` | Remove review app with `serverless` | `teardown:review` | -| `ServerlessDeploy` | Deploy production app with `serverless` | `deploy:production` | +### `ServerlessRun` + +Run serverless functions locally +#### Task options + +| Property | Description | Type | Default | +| :----------- | :---------------------------------------------------------- | :-------------- | :----------------- | +| `ports` | ports to try to bind to for this application | `Array` | `[3001,3002,3003]` | +| `useDoppler` | run the application with environment variables from Doppler | `boolean` | `true` | + +_All properties are optional._ + +### `ServerlessDeploy` + +Deploy a serverless function + +### `ServerlessProvision` + +Provision a review serverless function + +### `ServerlessTeardown` + +Tear down existing serverless functions + + +## Plugin-wide options + +### `@dotcom-tool-kit/serverless` + +| Property | Description | Type | Default | +| :---------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------- | :-------------- | +| **`awsAccountId`** (\*) | the ID of the AWS account you wish to deploy to (account IDs can be found at the [FT login page](https://awslogin.in.ft.com/)) | `string` | | +| **`systemCode`** (\*) | the system code for your app | `string` | | +| `regions` | an array of AWS regions you want to deploy to | `Array` | `["eu-west-1"]` | +| `configPath` | path to your serverless config file. If this is not provided, Serverless defaults to `./serverless.yml` but [other config fomats are accepted](https://www.serverless.com/framework/docs/providers/aws/guide/intro#alternative-configuration-format) | `string` | | + +_(\*) Required._ + diff --git a/plugins/serverless/src/index.ts b/plugins/serverless/src/index.ts deleted file mode 100644 index e5ae4008b..000000000 --- a/plugins/serverless/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -import ServerlessDeploy from './tasks/deploy' -import ServerlessProvision from './tasks/provision' -import ServerlessRun from './tasks/run' -import ServerlessTeardown from './tasks/teardown' - -export const tasks = [ServerlessRun, ServerlessDeploy, ServerlessProvision, ServerlessTeardown] diff --git a/plugins/serverless/src/tasks/deploy.ts b/plugins/serverless/src/tasks/deploy.ts index a4d3dfdfd..8e31575c1 100644 --- a/plugins/serverless/src/tasks/deploy.ts +++ b/plugins/serverless/src/tasks/deploy.ts @@ -1,44 +1,11 @@ -import { ToolKitError } from '@dotcom-tool-kit/error' -import { hookFork, styles, waitOnExit } from '@dotcom-tool-kit/logger' -import { Task } from '@dotcom-tool-kit/types' -import { ServerlessSchema } from '@dotcom-tool-kit/types/lib/schema/serverless' -import { DopplerEnvVars } from '@dotcom-tool-kit/doppler' -import { getOptions } from '@dotcom-tool-kit/options' +import { hookFork, waitOnExit } from '@dotcom-tool-kit/logger' +import { Task } from '@dotcom-tool-kit/base' +import { ServerlessSchema } from '@dotcom-tool-kit/schemas/lib/plugins/serverless' import { spawn } from 'child_process' -export default class ServerlessDeploy extends Task { - static description = 'Deploys on AWS' - +export default class ServerlessDeploy extends Task<{ plugin: typeof ServerlessSchema }> { async run(): Promise { - const { useVault, configPath, buildNumVariable, regions, systemCode } = this.options - const buildNum = process.env[buildNumVariable] - - if (buildNum === undefined) { - throw new ToolKitError( - `the ${styles.task('ServerlessDeploy')} task requires the ${styles.code( - `$${buildNumVariable}` - )} environment variable to be defined` - ) - } - - let vaultEnv = {} - // HACK:20231124:IM We need to call Vault to check whether a project has - // migrated to Doppler yet, and sync Vault secrets if it hasn't, but this - // logic should be removed entirely once we drop support for Vault. We can - // skip this call if we find the project has already added options for - // doppler in the Tool Kit configuration. - const migratedToolKitToDoppler = Boolean(getOptions('@dotcom-tool-kit/doppler')?.project) - if (useVault && !migratedToolKitToDoppler) { - const dopplerCi = new DopplerEnvVars(this.logger, 'ci') - const vaultCi = await dopplerCi.fallbackToVault() - // HACK:20231023:IM don't read secrets when the project has already - // migrated from Vault to Doppler – Doppler will instead sync secrets to - // Parameter Store for the Serverless config to reference - if (!vaultCi.MIGRATED_TO_DOPPLER) { - const dopplerEnvVars = new DopplerEnvVars(this.logger, 'prod') - vaultEnv = await dopplerEnvVars.fallbackToVault() - } - } + const { configPath, regions, systemCode } = this.pluginOptions for (const region of regions) { this.logger.verbose('starting the child serverless process...') @@ -56,10 +23,7 @@ export default class ServerlessDeploy extends Task { } const child = spawn('serverless', args, { - env: { - ...process.env, - ...vaultEnv - } + env: process.env }) hookFork(this.logger, 'serverless', child) diff --git a/plugins/serverless/src/tasks/provision.ts b/plugins/serverless/src/tasks/provision.ts index 1ad43485a..c0a7f466a 100644 --- a/plugins/serverless/src/tasks/provision.ts +++ b/plugins/serverless/src/tasks/provision.ts @@ -1,44 +1,33 @@ import { ToolKitError } from '@dotcom-tool-kit/error' import { hookFork, styles, waitOnExit } from '@dotcom-tool-kit/logger' -import { Task } from '@dotcom-tool-kit/types' -import { ServerlessSchema } from '@dotcom-tool-kit/types/lib/schema/serverless' -import { DopplerEnvVars } from '@dotcom-tool-kit/doppler' +import { Task } from '@dotcom-tool-kit/base' +import { ServerlessSchema } from '@dotcom-tool-kit/schemas/lib/plugins/serverless' import { spawn } from 'child_process' -import { getOptions } from '@dotcom-tool-kit/options' -import { writeState } from '@dotcom-tool-kit/state' - -export default class ServerlessProvision extends Task { - static description = 'Provisions a job on AWS' +import { readState, writeState } from '@dotcom-tool-kit/state' +export default class ServerlessProvision extends Task<{ plugin: typeof ServerlessSchema }> { async run(): Promise { - const { useVault, configPath, buildNumVariable, systemCode, regions } = this.options - const buildNum = process.env[buildNumVariable] + const { configPath, systemCode, regions } = this.pluginOptions + const ciState = readState('ci') - if (buildNum === undefined) { + if (!ciState) { throw new ToolKitError( - `the ${styles.task('ServerlessProvision')} task requires the ${styles.code( - `$${buildNumVariable}` - )} environment variable to be defined` + `the ${styles.task( + 'ServerlessDeploy' + )} should be run in CI, but no CI state was found. check you have a plugin installed that initialises the CI state.` ) } - let vaultEnv = {} - // HACK:20231124:IM We need to call Vault to check whether a project has - // migrated to Doppler yet, and sync Vault secrets if it hasn't, but this - // logic should be removed entirely once we drop support for Vault. We can - // skip this call if we find the project has already added options for - // doppler in the Tool Kit configuration. - const migratedToolKitToDoppler = Boolean(getOptions('@dotcom-tool-kit/doppler')?.project) - if (useVault && !migratedToolKitToDoppler) { - const dopplerCi = new DopplerEnvVars(this.logger, 'ci') - const vaultCi = await dopplerCi.fallbackToVault() - // HACK:20231023:IM don't read secrets when the project has already - // migrated from Vault to Doppler – Doppler will instead sync secrets to - // Parameter Store for the Serverless config to reference - if (!vaultCi.MIGRATED_TO_DOPPLER) { - const dopplerEnvVars = new DopplerEnvVars(this.logger, 'dev') - vaultEnv = await dopplerEnvVars.fallbackToVault() - } + const buildNum = ciState?.buildNumber + + if (!buildNum) { + const error = new ToolKitError( + `the ${styles.task('ServerlessDeploy')} requires a CI build number in the CI state.` + ) + + error.details = `this is provided by plugins such as ${styles.plugin( + 'circleci' + )}, which populates it from the CIRCLE_BUILD_NUM environment variable.` } const stageName = `ci${buildNum}` @@ -58,10 +47,7 @@ export default class ServerlessProvision extends Task { } const child = spawn('serverless', args, { - env: { - ...process.env, - ...vaultEnv - } + env: process.env }) hookFork(this.logger, 'serverless', child) diff --git a/plugins/serverless/src/tasks/run.ts b/plugins/serverless/src/tasks/run.ts index b64e7ba04..23de8c817 100644 --- a/plugins/serverless/src/tasks/run.ts +++ b/plugins/serverless/src/tasks/run.ts @@ -1,20 +1,23 @@ -import { Task } from '@dotcom-tool-kit/types' -import { ServerlessSchema } from '@dotcom-tool-kit/types/lib/schema/serverless' +import { Task } from '@dotcom-tool-kit/base' +import { ServerlessSchema } from '@dotcom-tool-kit/schemas/lib/plugins/serverless' +import { ServerlessRunSchema } from '@dotcom-tool-kit/schemas/src/tasks/serverless-run' import { spawn } from 'child_process' import { DopplerEnvVars } from '@dotcom-tool-kit/doppler' import { hookConsole, hookFork } from '@dotcom-tool-kit/logger' import getPort from 'get-port' import waitPort from 'wait-port' -export default class ServerlessRun extends Task { - static description = 'Run serverless functions locally' - +export default class ServerlessRun extends Task<{ + task: typeof ServerlessRunSchema + plugin: typeof ServerlessSchema +}> { async run(): Promise { - const { useVault, ports, configPath } = this.options + const { useDoppler, ports } = this.options + const { configPath } = this.pluginOptions let dopplerEnv = {} - if (useVault) { + if (useDoppler) { const doppler = new DopplerEnvVars(this.logger, 'dev') dopplerEnv = await doppler.get() diff --git a/plugins/serverless/src/tasks/teardown.ts b/plugins/serverless/src/tasks/teardown.ts index 0797d126c..94217a044 100644 --- a/plugins/serverless/src/tasks/teardown.ts +++ b/plugins/serverless/src/tasks/teardown.ts @@ -1,17 +1,13 @@ import { ToolKitError } from '@dotcom-tool-kit/error' import { hookFork, styles, waitOnExit } from '@dotcom-tool-kit/logger' -import { Task } from '@dotcom-tool-kit/types' -import { ServerlessSchema } from '@dotcom-tool-kit/types/lib/schema/serverless' +import { Task } from '@dotcom-tool-kit/base' +import { ServerlessSchema } from '@dotcom-tool-kit/schemas/lib/plugins/serverless' import { readState } from '@dotcom-tool-kit/state' -import { DopplerEnvVars } from '@dotcom-tool-kit/doppler' import { spawn } from 'child_process' -import { getOptions } from '@dotcom-tool-kit/options' - -export default class ServerlessTeardown extends Task { - static description = 'Teardown existing serverless functions' +export default class ServerlessTeardown extends Task<{ plugin: typeof ServerlessSchema }> { async run(): Promise { - const { useVault, configPath, regions, systemCode } = this.options + const { configPath, regions, systemCode } = this.pluginOptions const reviewState = readState('review') @@ -21,25 +17,6 @@ export default class ServerlessTeardown extends Task { ) } - let vaultEnv = {} - // HACK:20231124:IM We need to call Vault to check whether a project has - // migrated to Doppler yet, and sync Vault secrets if it hasn't, but this - // logic should be removed entirely once we drop support for Vault. We can - // skip this call if we find the project has already added options for - // doppler in the Tool Kit configuration. - const migratedToolKitToDoppler = Boolean(getOptions('@dotcom-tool-kit/doppler')?.project) - if (useVault && !migratedToolKitToDoppler) { - const dopplerCi = new DopplerEnvVars(this.logger, 'ci') - const vaultCi = await dopplerCi.fallbackToVault() - // HACK:20231023:IM don't read secrets when the project has already - // migrated from Vault to Doppler – Doppler will instead sync secrets to - // Parameter Store for the Serverless config to reference - if (!vaultCi.MIGRATED_TO_DOPPLER) { - const dopplerEnvVars = new DopplerEnvVars(this.logger, 'dev') - vaultEnv = await dopplerEnvVars.fallbackToVault() - } - } - this.logger.verbose('starting the child serverless process...') const args = [ @@ -56,10 +33,7 @@ export default class ServerlessTeardown extends Task { } const child = spawn('serverless', args, { - env: { - ...process.env, - ...vaultEnv - } + env: process.env }) hookFork(this.logger, 'serverless', child) diff --git a/plugins/serverless/tsconfig.json b/plugins/serverless/tsconfig.json index 11d6852cc..49eaf3274 100644 --- a/plugins/serverless/tsconfig.json +++ b/plugins/serverless/tsconfig.json @@ -6,7 +6,7 @@ }, "references": [ { - "path": "../../lib/types" + "path": "../../lib/base" }, { "path": "../../lib/doppler" @@ -16,7 +16,12 @@ }, { "path": "../../lib/options" + }, + { + "path": "../../lib/schemas" } ], - "include": ["src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/plugins/typescript/.toolkitrc.yml b/plugins/typescript/.toolkitrc.yml index 5f5ed2408..554680e73 100644 --- a/plugins/typescript/.toolkitrc.yml +++ b/plugins/typescript/.toolkitrc.yml @@ -1,6 +1,12 @@ -hooks: - 'build:local': TypeScriptBuild - 'build:ci': TypeScriptBuild - 'build:remote': TypeScriptBuild - 'run:local': TypeScriptWatch - 'test:local': TypeScriptTest +tasks: + TypeScript: './lib/tasks/typescript' + +commands: + # TODO add options here once we support per-command-assignment options + 'build:local': TypeScript + 'build:ci': TypeScript + 'build:remote': TypeScript + 'run:local': TypeScript # watch + 'test:local': TypeScript # noEmit + +version: 2 diff --git a/plugins/typescript/jest.config.js b/plugins/typescript/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/typescript/jest.config.js +++ b/plugins/typescript/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/typescript/package.json b/plugins/typescript/package.json index a993977e7..b522763f6 100644 --- a/plugins/typescript/package.json +++ b/plugins/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/typescript", - "version": "2.2.0", + "version": "3.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -21,20 +21,21 @@ ".toolkitrc.yml" ], "peerDependencies": { - "dotcom-tool-kit": "3.x", - "typescript": "3.x || 4.x" + "dotcom-tool-kit": "4.0.0-beta.5", + "typescript": "3.x || 4.x || 5.x" }, "dependencies": { - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0" + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^29.3.1", "typescript": "^4.9.4", "winston": "^3.8.2" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/typescript/readme.md b/plugins/typescript/readme.md index a0de9f07d..9162ba753 100644 --- a/plugins/typescript/readme.md +++ b/plugins/typescript/readme.md @@ -17,17 +17,20 @@ plugins: - '@dotcom-tool-kit/typescript' ``` -## Options + +## Tasks -| Key | Description | Default value | -|-|-|-| -| `configPath` | Path to the [TypeScript config file](https://www.typescriptlang.org/tsconfig) | use TypeScript's own [tsconfig.json resolution](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#using-tsconfigjson-or-jsconfigjson) | -| `extraArgs` | Extra [arguments](https://www.typescriptlang.org/docs/handbook/compiler-options.html) to pass to the tsc CLI that can't be set in `tsconfig.json` | `[]` +### `TypeScript` -## Tasks +Compile code with `tsc`. +#### Task options + +| Property | Description | Type | +| :----------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | +| `configPath` | to the [TypeScript config file](https://www.typescriptlang.org/tsconfig). Uses TypeScript's own [tsconfig.json resolution](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html#using-tsconfigjson-or-jsconfigjson) by default | `string` | +| `build` | Run Typescript in [build mode](https://www.typescriptlang.org/docs/handbook/project-references.html#build-mode-for-typescript). | `true` | +| `watch` | Run Typescript in watch mode. | `true` | +| `noEmit` | Run Typescript with `--noEmit`, for checking your types without outputting compiled Javascript. | `true` | -| Task | Description | Preconfigured hook | -|-|-|-| -| `TypeScriptBuild` | runs `tsc` to compile TypeScript to JavaScript | `build:local`, `build:ci`, `build:remote` | -| `TypeScriptWatch` | rebuild project on every project file change | `run:local` | -| `TypeScriptTest` | type check TypeScript code without emitting code | `test:local` | +_All properties are optional._ + diff --git a/plugins/typescript/src/index.ts b/plugins/typescript/src/index.ts deleted file mode 100644 index 41e60f3e0..000000000 --- a/plugins/typescript/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { TypeScriptBuild, TypeScriptWatch, TypeScriptTest } from './tasks/typescript' - -export const tasks = [TypeScriptBuild, TypeScriptWatch, TypeScriptTest] diff --git a/plugins/typescript/src/tasks/typescript.ts b/plugins/typescript/src/tasks/typescript.ts index 10c6b08d6..883b3b7b7 100644 --- a/plugins/typescript/src/tasks/typescript.ts +++ b/plugins/typescript/src/tasks/typescript.ts @@ -1,21 +1,29 @@ import { hookFork, waitOnExit } from '@dotcom-tool-kit/logger' -import { Task } from '@dotcom-tool-kit/types' -import type { TypeScriptSchema } from '@dotcom-tool-kit/types/lib/schema/typescript' +import { Task } from '@dotcom-tool-kit/base' +import type { TypeScriptSchema } from '@dotcom-tool-kit/schemas/lib/tasks/typescript' import { fork } from 'child_process' const tscPath = require.resolve('typescript/bin/tsc') -abstract class TypeScriptTask extends Task { - abstract taskArgs: string[] - +export default class TypeScript extends Task<{ task: typeof TypeScriptSchema }> { async run(): Promise { - // TODO: add monorepo support with --build option - const args = [...this.taskArgs] - if (this.options.configPath) { - args.unshift('--project', this.options.configPath) + const args = [] + + // TODO:KB:20240408 refactor this + if (this.options.build) { + args.push('--build') + } + + if (this.options.watch) { + args.push('--watch') } - if (this.options.extraArgs) { - args.push(...this.options.extraArgs) + + if (this.options.noEmit) { + args.push('--noEmit') + } + + if (this.options.configPath) { + args.push('--project', this.options.configPath) } const child = fork(tscPath, args, { silent: true }) @@ -30,21 +38,3 @@ abstract class TypeScriptTask extends Task { this.logger.info('code compiled successfully') } } - -export class TypeScriptBuild extends TypeScriptTask { - static description = 'compile TypeScript to JavaScript' - - taskArgs = [] -} - -export class TypeScriptWatch extends TypeScriptTask { - static description = 'rebuild TypeScript project every file change' - - taskArgs = ['--watch'] -} - -export class TypeScriptTest extends TypeScriptTask { - static description = 'type check TypeScript code' - - taskArgs = ['--noEmit'] -} diff --git a/plugins/typescript/test/tasks/typescript.test.ts b/plugins/typescript/test/tasks/typescript.test.ts index f52fef97e..2cecff491 100644 --- a/plugins/typescript/test/tasks/typescript.test.ts +++ b/plugins/typescript/test/tasks/typescript.test.ts @@ -1,10 +1,10 @@ import { describe, jest, it, expect } from '@jest/globals' -import { TypeScriptBuild, TypeScriptWatch, TypeScriptTest } from '../../src/tasks/typescript' +import TypeScript from '../../src/tasks/typescript' import { fork } from 'child_process' import EventEmitter from 'events' import winston, { Logger } from 'winston' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger jest.mock('child_process', () => ({ fork: jest.fn(() => { @@ -22,36 +22,45 @@ const tscPath = require.resolve('typescript/bin/tsc') const configPath = 'tsconfig.json' describe('typescript', () => { - describe('correct arguments', () => { - it('should start tsc build with correct arguments', async () => { - const task = new TypeScriptBuild(logger, { configPath }) - await task.run() + it('should run tsc', async () => { + const task = new TypeScript(logger, 'TypeScript', {}, { configPath }) + await task.run() - expect(fork).toBeCalledWith(tscPath, ['--project', configPath], { silent: true }) - }) + expect(fork).toBeCalledWith(tscPath, ['--project', configPath], { silent: true }) + }) - it('should start tsc watch with correct arguments', async () => { - const task = new TypeScriptWatch(logger, { configPath }) - await task.run() + it('watch option should run tsc with --watch arg', async () => { + const task = new TypeScript(logger, 'TypeScript', {}, { configPath, watch: true }) + await task.run() - expect(fork).toBeCalledWith(tscPath, ['--project', configPath, '--watch'], { silent: true }) - }) + expect(fork).toBeCalledWith(tscPath, ['--watch', '--project', configPath], { silent: true }) + }) - it('should start tsc test with correct arguments', async () => { - const task = new TypeScriptTest(logger, { configPath }) - await task.run() + it('noEmit option should run tsc with --noEmit arg', async () => { + const task = new TypeScript(logger, 'TypeScript', {}, { configPath, noEmit: true }) + await task.run() - expect(fork).toBeCalledWith(tscPath, ['--project', configPath, '--noEmit'], { silent: true }) - }) + expect(fork).toBeCalledWith(tscPath, ['--noEmit', '--project', configPath], { silent: true }) + }) + + it('build option should run tsc with --build arg', async () => { + const task = new TypeScript(logger, 'TypeScript', {}, { configPath, build: true }) + await task.run() + + expect(fork).toBeCalledWith(tscPath, ['--build', '--project', configPath], { silent: true }) + }) - it('should pass in extra arguments', async () => { - const extraArgs = ['--verbose', '--force'] - const task = new TypeScriptBuild(logger, { configPath, extraArgs }) - await task.run() + it('can combine options', async () => { + const task = new TypeScript( + logger, + 'TypeScript', + {}, + { configPath, build: true, watch: true, noEmit: true } + ) + await task.run() - expect(fork).toBeCalledWith(tscPath, ['--project', configPath, ...extraArgs], { - silent: true - }) + expect(fork).toBeCalledWith(tscPath, ['--build', '--watch', '--noEmit', '--project', configPath], { + silent: true }) }) }) diff --git a/plugins/typescript/tsconfig.json b/plugins/typescript/tsconfig.json index 0553e6343..c8906d23a 100644 --- a/plugins/typescript/tsconfig.json +++ b/plugins/typescript/tsconfig.json @@ -9,8 +9,13 @@ "path": "../../lib/logger" }, { - "path": "../../lib/types" + "path": "../../lib/base" + }, + { + "path": "../../lib/schemas" } ], - "include": ["src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/plugins/upload-assets-to-s3/.toolkitrc.yml b/plugins/upload-assets-to-s3/.toolkitrc.yml index c9222944d..a4754df00 100644 --- a/plugins/upload-assets-to-s3/.toolkitrc.yml +++ b/plugins/upload-assets-to-s3/.toolkitrc.yml @@ -1,2 +1,7 @@ -hooks: +tasks: + UploadAssetsToS3: './lib/tasks/upload-assets-to-s3' + +commands: 'release:remote': UploadAssetsToS3 + +version: 2 diff --git a/plugins/upload-assets-to-s3/README.md b/plugins/upload-assets-to-s3/README.md index e8466c8af..11606078f 100644 --- a/plugins/upload-assets-to-s3/README.md +++ b/plugins/upload-assets-to-s3/README.md @@ -17,32 +17,28 @@ plugins: - '@dotcom-tool-kit/upload-assets-to-s3' ``` + +## Tasks -## Options -| Key | Description | Default value | -|-|-|-| -| `accessKeyIdEnvVar` | variable name of the project's aws access key id. If uploading to multiple buckets the same credentials will need to work for all. | no default value - for backwards compatability the plugin falls back to the default value for `accessKeyId` | -| `secretAccessKeyEnvVar` | variable name of the project's aws secret access key | no default value - for backwards compatability the plugin falls back to the default value for `secretAccessKey` | -| `accessKeyId` | **DEPRECATED** variable name of the project's aws access key id | 'aws_access_hashed_assets' | -| `secretAccessKey` | **DEPRECATED** variable name of the project's aws secret access key | 'aws_secret_hashed_assets' | -| `directory` | the folder in the project whose contents will be uploaded to S3 | 'public' | -| `reviewBucket` | the development or test S3 bucket | `['ft-next-hashed-assets-preview']` | -| `prodBucket` | production S3 bucket/s; an array of strings. The same files will be uploaded to each. _Note: most Customer Products buckets that have a `prod` and `prod-us` version are already configured in AWS to replicate file changes from one to the other so you don't need to specify both here. Also, if multiple buckets are specified the same credentials will need to be valid for both for the upload to be successful_ | `['ft-next-hashed-assets-prod']` | -| `region` | the AWS region your buckets are stored in (let the Platforms team know if you need to upload to multiple buckets in multiple regions) | eu-west-1 | -| `destination` | the destination folder for uploaded assets. Set to `''` to upload assets to the top level of the bucket | 'hashed-assets/page-kit' | -| `extensions` | file extensions to be uploaded to S3 | 'js,css,map,gz,br,png,jpg,jpeg,gif,webp,svg,ico,json' | -| `cacheControl` | header that controls how long your files stay in a CloudFront cache before CloudFront forwards another request to your origin | 'public, max-age=31536000, stale-while-revalidate=60, stale-if-error=3600' | - -Example: -```yml -'@dotcom-tool-kit/upload-assets-to-s3': - '@dotcom-tool-kit/upload-assets-to-s3': - accessKeyId: AWS_ACCESS - secretAccessKey: AWS_KEY - prodBucket: ['ft-next-service-registry-prod'] - reviewBucket: ['ft-next-service-registry-dev'] - destination: '' -``` +### `UploadAssetsToS3` + +Upload files to an AWS S3 bucket. +#### Task options + +| Property | Description | Type | Default | +| :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------- | :--------------------------------------------------------------------------- | +| `accessKeyIdEnvVar` | variable name of the project's aws access key id. If uploading to multiple buckets the same credentials will need to work for all | `string` | `'AWS_ACCESS_HASHED_ASSETS'` | +| `secretAccessKeyEnvVar` | variable name of the project's aws secret access key | `string` | `'AWS_SECRET_HASHED_ASSETS'` | +| `directory` | the folder in the project whose contents will be uploaded to S3 | `string` | `'public'` | +| `reviewBucket` | the development or test S3 bucket | `Array` | `["ft-next-hashed-assets-preview"]` | +| `prodBucket` | production S3 bucket(s). The same files will be uploaded to each. **Note**: most Customer Products buckets that have a `prod` and `prod-us` version are already configured in AWS to replicate file changes from one to the other so you don't need to specify both here. Also, if multiple buckets are specified the same credentials will need to be valid for both for the upload to be successful. | `Array` | `["ft-next-hashed-assets-prod"]` | +| `region` | the AWS region your buckets are stored in (let the Platforms team know if you need to upload to multiple buckets in multiple regions). | `string` | `'eu-west-1'` | +| `destination` | the destination folder for uploaded assets. Set to `''` to upload assets to the top level of the bucket | `string` | `'hashed-assets/page-kit'` | +| `extensions` | file extensions to be uploaded to S3 | `string` | `'js,css,map,gz,br,png,jpg,jpeg,gif,webp,svg,ico,json'` | +| `cacheControl` | header that controls how long your files stay in a CloudFront cache before CloudFront forwards another request to your origin | `string` | `'public, max-age=31536000, stale-while-revalidate=60, stale-if-error=3600'` | + +_All properties are optional._ + ## Testing uploads using the review bucket diff --git a/plugins/upload-assets-to-s3/jest.config.js b/plugins/upload-assets-to-s3/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/upload-assets-to-s3/jest.config.js +++ b/plugins/upload-assets-to-s3/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/upload-assets-to-s3/package.json b/plugins/upload-assets-to-s3/package.json index 194f7e1f1..aa8d8bd94 100644 --- a/plugins/upload-assets-to-s3/package.json +++ b/plugins/upload-assets-to-s3/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/upload-assets-to-s3", - "version": "3.2.0", + "version": "4.0.0-beta.5", "description": "", "main": "lib", "scripts": { @@ -11,9 +11,9 @@ "license": "ISC", "dependencies": { "@aws-sdk/client-s3": "^3.256.0", - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "glob": "^7.1.6", "mime": "^2.5.2", "tslib": "^2.3.1" @@ -27,6 +27,7 @@ "homepage": "https://github.com/financial-times/dotcom-tool-kit/tree/main/plugins/upload-assets-to-s3", "devDependencies": { "@aws-sdk/types": "^3.13.1", + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "@types/glob": "^7.1.3", "@types/jest": "^27.4.0", @@ -41,10 +42,10 @@ "extends": "../../package.json" }, "peerDependencies": { - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/upload-assets-to-s3/src/index.ts b/plugins/upload-assets-to-s3/src/index.ts deleted file mode 100644 index 28d683f7a..000000000 --- a/plugins/upload-assets-to-s3/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import UploadAssetsToS3 from './tasks/upload-assets-to-s3' - -export const tasks = [UploadAssetsToS3] diff --git a/plugins/upload-assets-to-s3/src/tasks/upload-assets-to-s3.ts b/plugins/upload-assets-to-s3/src/tasks/upload-assets-to-s3.ts index 698144b63..ddfe5e419 100644 --- a/plugins/upload-assets-to-s3/src/tasks/upload-assets-to-s3.ts +++ b/plugins/upload-assets-to-s3/src/tasks/upload-assets-to-s3.ts @@ -1,4 +1,4 @@ -import { Task } from '@dotcom-tool-kit/types' +import { Task } from '@dotcom-tool-kit/base' import * as fs from 'fs' import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3' import path from 'path' @@ -9,11 +9,9 @@ import { styles } from '@dotcom-tool-kit/logger' import { UploadAssetsToS3Options, UploadAssetsToS3Schema -} from '@dotcom-tool-kit/types/lib/schema/upload-assets-to-s3' - -export default class UploadAssetsToS3 extends Task { - static description = '' +} from '@dotcom-tool-kit/schemas/lib/tasks/upload-assets-to-s3' +export default class UploadAssetsToS3 extends Task<{ task: typeof UploadAssetsToS3Schema }> { async run(): Promise { await this.uploadAssetsToS3(this.options) } @@ -76,24 +74,29 @@ export default class UploadAssetsToS3 extends Task { - return process.env[envName] ?? process.env[envName.toUpperCase()] + const accessKeyId = process.env[options.accessKeyIdEnvVar] + const secretAccessKey = process.env[options.secretAccessKeyEnvVar] + + if (!accessKeyId || !secretAccessKey) { + const missingVars = [ + !accessKeyId ? options.accessKeyIdEnvVar : false, + !secretAccessKey ? options.secretAccessKeyEnvVar : false + ] + + const error = new ToolKitError( + `environment variable${missingVars.length > 1 ? 's' : ''} ${missingVars.join(' and ')} not set` + ) + error.details = `if your AWS credentials are stored in different environment variables, set the ${styles.code( + 'accessKeyIdEnvVar' + )} and ${styles.code('secretAccessKeyEnvVar')} options for this task.` + throw error } + const s3 = new S3Client({ region: options.region, - // will fallback to default value for accessKeyId if neither - // accessKeyIdEnvVar nor accessKeyId have been provided as options credentials: { - accessKeyId: - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - checkUppercaseName(options.accessKeyIdEnvVar ?? options.accessKeyId)!, - secretAccessKey: - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - checkUppercaseName(options.secretAccessKeyEnvVar ?? options.secretAccessKey)! + accessKeyId, + secretAccessKey } }) diff --git a/plugins/upload-assets-to-s3/test/tasks/upload-assets-to-s3.test.ts b/plugins/upload-assets-to-s3/test/tasks/upload-assets-to-s3.test.ts index 7dfce346b..a6e98379f 100644 --- a/plugins/upload-assets-to-s3/test/tasks/upload-assets-to-s3.test.ts +++ b/plugins/upload-assets-to-s3/test/tasks/upload-assets-to-s3.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach } from '@jest/globals' import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3' -import { UploadAssetsToS3Options } from '@dotcom-tool-kit/types/lib/schema/upload-assets-to-s3' +import type { UploadAssetsToS3Options } from '@dotcom-tool-kit/schemas/lib/tasks/upload-assets-to-s3' import * as path from 'path' import winston, { Logger } from 'winston' import UploadAssetsToS3 from '../../src/tasks/upload-assets-to-s3' @@ -8,13 +8,13 @@ jest.mock('@aws-sdk/client-s3') const mockedS3Client = jest.mocked(S3Client, true) const mockedPutObjectCommand = jest.mocked(PutObjectCommand, true) -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger const testDirectory = path.join(__dirname, '../files') const defaults: UploadAssetsToS3Options = { - accessKeyId: 'aws_access_hashed_assets', - secretAccessKey: 'aws_secret_hashed_assets', + accessKeyIdEnvVar: 'AWS_ACCESS_HASHED_ASSETS', + secretAccessKeyEnvVar: 'AWS_SECRET_HASHED_ASSETS', directory: 'public', reviewBucket: ['ft-next-hashed-assets-preview'], prodBucket: ['ft-next-hashed-assets-prod'], @@ -25,18 +25,52 @@ const defaults: UploadAssetsToS3Options = { } describe('upload-assets-to-s3', () => { + let oldEnv: Record + beforeEach(() => { + oldEnv = { ...process.env } + mockedS3Client.prototype.send.mockReturnValue({ promise: jest.fn().mockResolvedValue('mock upload complete') } as any) // eslint-disable-line @typescript-eslint/no-explicit-any }) + afterEach(() => { + process.env = oldEnv + }) + + it('should throw an error if env vars are not set', async () => { + const task = new UploadAssetsToS3( + logger, + 'UploadAssetsToS3', + {}, + { + ...defaults, + directory: testDirectory + } + ) + process.env.NODE_ENV = 'branch' + process.env.AWS_ACCESS_HASHED_ASSETS = '' + process.env.AWS_SECRET_HASHED_ASSETS = '' + + await expect(task.run()).rejects.toThrowErrorMatchingInlineSnapshot( + `"environment variables AWS_ACCESS_HASHED_ASSETS and AWS_SECRET_HASHED_ASSETS not set"` + ) + }) + it('should upload all globbed files for review', async () => { - const task = new UploadAssetsToS3(logger, { - ...defaults, - directory: testDirectory - }) + const task = new UploadAssetsToS3( + logger, + 'UploadAssetsToS3', + {}, + { + ...defaults, + directory: testDirectory + } + ) process.env.NODE_ENV = 'branch' + process.env.AWS_ACCESS_HASHED_ASSETS = 'access' + process.env.AWS_SECRET_HASHED_ASSETS = 'secret' await task.run() @@ -45,11 +79,18 @@ describe('upload-assets-to-s3', () => { }) it('should upload all globbed files for prod', async () => { - const task = new UploadAssetsToS3(logger, { - ...defaults, - directory: testDirectory - }) + const task = new UploadAssetsToS3( + logger, + 'UploadAssetsToS3', + {}, + { + ...defaults, + directory: testDirectory + } + ) process.env.NODE_ENV = 'production' + process.env.AWS_ACCESS_HASHED_ASSETS = 'access' + process.env.AWS_SECRET_HASHED_ASSETS = 'secret' await task.run() @@ -58,13 +99,20 @@ describe('upload-assets-to-s3', () => { }) it('should strip base path from S3 key', async () => { - const task = new UploadAssetsToS3(logger, { - ...defaults, - extensions: 'gz', - directory: testDirectory, - destination: 'testdir' - }) + const task = new UploadAssetsToS3( + logger, + 'UploadAssetsToS3', + {}, + { + ...defaults, + extensions: 'gz', + directory: testDirectory, + destination: 'testdir' + } + ) process.env.NODE_ENV = 'production' + process.env.AWS_ACCESS_HASHED_ASSETS = 'access' + process.env.AWS_SECRET_HASHED_ASSETS = 'secret' await task.run() const s3 = jest.mocked(mockedS3Client.mock.instances[0]) @@ -73,12 +121,19 @@ describe('upload-assets-to-s3', () => { }) it('should use correct Content-Encoding for compressed files', async () => { - const task = new UploadAssetsToS3(logger, { - ...defaults, - extensions: 'gz', - directory: testDirectory - }) + const task = new UploadAssetsToS3( + logger, + 'UploadAssetsToS3', + {}, + { + ...defaults, + extensions: 'gz', + directory: testDirectory + } + ) process.env.NODE_ENV = 'production' + process.env.AWS_ACCESS_HASHED_ASSETS = 'access' + process.env.AWS_SECRET_HASHED_ASSETS = 'secret' await task.run() @@ -93,34 +148,19 @@ describe('upload-assets-to-s3', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(mockedS3Client.prototype.send as any).mockRejectedValue(new Error(mockError)) - const task = new UploadAssetsToS3(logger, { - ...defaults, - directory: testDirectory - }) - - expect.assertions(1) - try { - await task.run() - } catch (e) { - expect(e.details).toEqual(mockError) - } - }) - - // HACK:20231006:IM make sure hack to support Doppler migration works - it('should fallback to uppercase environment variable', async () => { - const task = new UploadAssetsToS3(logger, { - ...defaults, - directory: testDirectory - }) - // must use delete to ensure an environment variable is undefined, setting - // 'undefined' will just stringify the word - delete process.env.aws_access_hashed_assets - process.env.AWS_ACCESS_HASHED_ASSETS = '1234' + const task = new UploadAssetsToS3( + logger, + 'UploadAssetsToS3', + {}, + { + ...defaults, + directory: testDirectory + } + ) - await task.run() + process.env.AWS_ACCESS_HASHED_ASSETS = 'access' + process.env.AWS_SECRET_HASHED_ASSETS = 'secret' - expect(mockedS3Client).toHaveBeenCalledWith( - expect.objectContaining({ credentials: expect.objectContaining({ accessKeyId: '1234' }) }) - ) + await expect(task.run()).rejects.toThrow('ft-next-hashed-assets-prod failed') }) }) diff --git a/plugins/upload-assets-to-s3/tsconfig.json b/plugins/upload-assets-to-s3/tsconfig.json index ec7a118bb..1b8e043f8 100644 --- a/plugins/upload-assets-to-s3/tsconfig.json +++ b/plugins/upload-assets-to-s3/tsconfig.json @@ -12,8 +12,13 @@ "path": "../../lib/logger" }, { - "path": "../../lib/types" + "path": "../../lib/base" + }, + { + "path": "../../lib/schemas" } ], - "include": ["src/**/*"] + "include": [ + "src/**/*" + ] } diff --git a/plugins/webpack/.toolkitrc.yml b/plugins/webpack/.toolkitrc.yml index d91271130..1a16c57e5 100644 --- a/plugins/webpack/.toolkitrc.yml +++ b/plugins/webpack/.toolkitrc.yml @@ -1,5 +1,10 @@ -hooks: - 'build:local': WebpackDevelopment - 'build:ci': WebpackProduction - 'build:remote': WebpackProduction - 'run:local': WebpackWatch +tasks: + Webpack: './lib/tasks/webpack' + +commands: + 'build:local': Webpack + 'build:ci': Webpack + 'build:remote': Webpack + 'run:local': Webpack + +version: 2 diff --git a/plugins/webpack/jest.config.js b/plugins/webpack/jest.config.js index 24889bbe6..2ea38bb31 100644 --- a/plugins/webpack/jest.config.js +++ b/plugins/webpack/jest.config.js @@ -1,5 +1,5 @@ const base = require('../../jest.config.base') module.exports = { - ...base + ...base.config } diff --git a/plugins/webpack/package.json b/plugins/webpack/package.json index db60df547..15bfc4132 100644 --- a/plugins/webpack/package.json +++ b/plugins/webpack/package.json @@ -1,6 +1,6 @@ { "name": "@dotcom-tool-kit/webpack", - "version": "3.2.0", + "version": "4.0.0-beta.5", "main": "lib", "description": "", "author": "FT.com Platforms Team ", @@ -17,17 +17,18 @@ "test": "cd ../../ ; npx jest --silent --projects plugins/webpack" }, "dependencies": { - "@dotcom-tool-kit/error": "^3.2.0", - "@dotcom-tool-kit/logger": "^3.4.0", - "@dotcom-tool-kit/types": "^3.6.0", + "@dotcom-tool-kit/base": "4.0.0-beta.0", + "@dotcom-tool-kit/error": "4.0.0-beta.0", + "@dotcom-tool-kit/logger": "4.0.0-beta.0", "webpack-cli": "^4.6.0", "tslib": "^2.3.1" }, "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "dotcom-tool-kit": "3.x" + "dotcom-tool-kit": "4.0.0-beta.5", + "webpack": "4.x.x || 5.x.x" }, "devDependencies": { + "@dotcom-tool-kit/schemas": "2.0.0-beta.0", "@jest/globals": "^27.4.6", "ts-node": "^10.0.0", "webpack": "^4.42.1", @@ -41,7 +42,7 @@ "extends": "../../package.json" }, "engines": { - "node": "16.x || 18.x || 20.x", + "node": "18.x || 20.x", "npm": "7.x || 8.x || 9.x || 10.x" } } diff --git a/plugins/webpack/readme.md b/plugins/webpack/readme.md index 54951014b..67898bc3d 100644 --- a/plugins/webpack/readme.md +++ b/plugins/webpack/readme.md @@ -17,37 +17,40 @@ plugins: - '@dotcom-tool-kit/webpack' ``` -You will need plugins that provides hooks to run the `Webpack*` tasks. - ### Building with Webpack locally -For local development, by default the `WebpackDevelopment` task runs on the `build:local` hook, and `WebpackWatch` runs on `run:local`. One plugin that provides these hooks is [`npm`](../npm), allowing you to run Webpack with `npm run build` and `npm start`. `WebpackWatch` runs Webpack in the background, allowing it to run alongside your other tasks that run on `run:local`, which lets you run e.g. your app with the [`node`](../node) plugin in parallel with Webpack. +For local development, by default the `WebpackDevelopment` task runs on the `build:local` command, and `WebpackWatch` runs on `run:local`. One plugin that provides these commands is [`npm`](../npm), allowing you to run Webpack with `npm run build` and `npm start`. `WebpackWatch` runs Webpack in the background, allowing it to run alongside your other tasks that run on `run:local`, which lets you run e.g. your app with the [`node`](../node) plugin in parallel with Webpack. ### Building with Webpack on CI and remote apps -The `WebpackProduction` task runs on the `build:ci` and `build:remote` hooks by default. `build:ci` is for compiling an app's source in CI jobs, and is provided by plugins like [`circleci`](../circleci/). `build:remote` compiles an app for running on a production or testing server, and can be provided by plugins like [`heroku`](../heroku/). +The `WebpackProduction` task runs on the `build:ci` and `build:remote` commands by default. `build:ci` is for compiling an app's source in CI jobs, and is provided by plugins like [`circleci`](../circleci/). `build:remote` compiles an app for running on a production or testing server, and can be provided by plugins like [`heroku`](../heroku/). -### Running on another hook -You can also configure Webpack to run on any other hook; for example, if you want to run it with `npm run test` via the `npm` plugin, you can manually configure Webpack to run on `npm`'s `test:local` hook: +### Running on another command +You can also configure Webpack to run on any other command; for example, if you want to run it with `npm run test` via the `npm` plugin, you can manually configure Webpack to run on `npm`'s `test:local` command: ```yml plugins: - '@dotcom-tool-kit/webpack' - '@dotcom-tool-kit/npm' -hooks: - 'test:local': WebpackDevelopment +command: + 'test:local': + Webpack: + mode: development ``` -## Options + +## Tasks + +### `Webpack` -| Key | Description | Default value | -|-|-|-| -| `configPath` | An optional path to your Webpack config file | none | +Bundle code with `webpack`. +#### Task options -## Tasks +| Property | Description | Type | +| :----------------- | :-------------------------------------------------------------------------- | :------------------------------ | +| `configPath` | path to a Webpack config file. Webpack will default to `webpack.config.js`. | `string` | +| **`envName`** (\*) | set Webpack's [mode](https://webpack.js.org/configuration/mode/). | `'production' \| 'development'` | +| `watch` | run Webpack in watch mode | `boolean` | -| Task | Description | Default hooks | -|-|-|-| -| `WebpackDevelopment` | Run Webpack in development mode | `build:local` | -| `WebpackProduction` | Run Webpack in production mode | `build:ci`, `build:remote` | -| `WebpackWatch` | Run Webpack in watch mode in the background | `run:local` | +_(\*) Required._ + diff --git a/plugins/webpack/src/index.ts b/plugins/webpack/src/index.ts deleted file mode 100644 index bc5b12f99..000000000 --- a/plugins/webpack/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import WebpackDevelopment from './tasks/development' -import WebpackProduction from './tasks/production' -import WebpackWatch from './tasks/watch' - -export const tasks = [WebpackDevelopment, WebpackProduction, WebpackWatch] diff --git a/plugins/webpack/src/run-webpack.ts b/plugins/webpack/src/run-webpack.ts deleted file mode 100644 index f0ff8b745..000000000 --- a/plugins/webpack/src/run-webpack.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { fork } from 'child_process' -import { Logger } from 'winston' -import type { WebpackOptions } from '@dotcom-tool-kit/types/lib/schema/webpack' -import { hookFork, waitOnExit } from '@dotcom-tool-kit/logger' - -const webpackCLIPath = require.resolve('webpack-cli/bin/cli') - -export interface RunWebpackOptions { - mode: 'production' | 'development' - watch?: boolean -} - -export default function runWebpack( - logger: Logger, - options: WebpackOptions & RunWebpackOptions -): Promise { - logger.info('starting Webpack...') - const args = ['build', '--color', `--mode=${options.mode}`] - - if (options.configPath) { - args.push(`--config=${options.configPath}`) - } - - if (options.watch) { - args.push('--watch') - } - - let { execArgv } = process - if (process.allowedNodeEnvironmentFlags.has('--openssl-legacy-provider')) { - // webpack 4 uses a legacy hashing function that is no longer provided by - // default in OpenSSL 3: https://github.com/webpack/webpack/issues/14532 - execArgv = [...execArgv, '--openssl-legacy-provider'] - } - - const child = fork(webpackCLIPath, args, { silent: true, execArgv }) - hookFork(logger, 'webpack', child) - return waitOnExit('webpack', child) -} diff --git a/plugins/webpack/src/tasks/development.ts b/plugins/webpack/src/tasks/development.ts deleted file mode 100644 index 25e1cf2d3..000000000 --- a/plugins/webpack/src/tasks/development.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Task } from '@dotcom-tool-kit/types' -import { WebpackSchema } from '@dotcom-tool-kit/types/lib/schema/webpack' -import runWebpack from '../run-webpack' - -export default class WebpackDevelopment extends Task { - static description = 'Run Webpack in development mode' - - async run(): Promise { - await runWebpack(this.logger, { - ...this.options, - mode: 'development' - }) - } -} diff --git a/plugins/webpack/src/tasks/production.ts b/plugins/webpack/src/tasks/production.ts deleted file mode 100644 index 882de4a22..000000000 --- a/plugins/webpack/src/tasks/production.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Task } from '@dotcom-tool-kit/types' -import { WebpackSchema } from '@dotcom-tool-kit/types/lib/schema/webpack' -import runWebpack from '../run-webpack' - -export default class WebpackProduction extends Task { - static description = 'Run Webpack in production mode' - - async run(): Promise { - await runWebpack(this.logger, { - ...this.options, - mode: 'production' - }) - } -} diff --git a/plugins/webpack/src/tasks/watch.ts b/plugins/webpack/src/tasks/watch.ts deleted file mode 100644 index 79d8f344e..000000000 --- a/plugins/webpack/src/tasks/watch.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Task } from '@dotcom-tool-kit/types' -import { WebpackSchema } from '@dotcom-tool-kit/types/lib/schema/webpack' -import runWebpack from '../run-webpack' - -export default class WebpackWatch extends Task { - static description = 'Run Webpack in watch mode in the background' - - async run(): Promise { - // don't wait for Webpack to exit, to leave it running in the background - runWebpack(this.logger, { - ...this.options, - mode: 'development', - watch: true - }) - } -} diff --git a/plugins/webpack/src/tasks/webpack.ts b/plugins/webpack/src/tasks/webpack.ts new file mode 100644 index 000000000..130454799 --- /dev/null +++ b/plugins/webpack/src/tasks/webpack.ts @@ -0,0 +1,32 @@ +import { type WebpackSchema } from '@dotcom-tool-kit/schemas/lib/tasks/webpack' +import { Task } from '@dotcom-tool-kit/base' +import { hookFork, waitOnExit } from '@dotcom-tool-kit/logger' +import { fork } from 'child_process' + +const webpackCLIPath = require.resolve('webpack-cli/bin/cli') + +export default class Webpack extends Task<{ task: typeof WebpackSchema }> { + async run(): Promise { + this.logger.info('starting Webpack...') + const args = ['build', '--color', `--mode=${this.options.envName}`] + + if (this.options.configPath) { + args.push(`--config=${this.options.configPath}`) + } + + if (this.options.watch) { + args.push('--watch') + } + + let { execArgv } = process + if (process.allowedNodeEnvironmentFlags.has('--openssl-legacy-provider')) { + // webpack 4 uses a legacy hashing function that is no longer provided by + // default in OpenSSL 3: https://github.com/webpack/webpack/issues/14532 + execArgv = [...execArgv, '--openssl-legacy-provider'] + } + + const child = fork(webpackCLIPath, args, { silent: true, execArgv }) + hookFork(this.logger, 'webpack', child) + return waitOnExit('webpack', child) + } +} diff --git a/plugins/webpack/test/tasks/webpack.test.ts b/plugins/webpack/test/tasks/webpack.test.ts index a458f3da0..04b135c0f 100644 --- a/plugins/webpack/test/tasks/webpack.test.ts +++ b/plugins/webpack/test/tasks/webpack.test.ts @@ -1,11 +1,10 @@ import { describe, jest, it, expect } from '@jest/globals' -import DevelopmentWebpack from '../../src/tasks/development' -import ProductionWebpack from '../../src/tasks/production' +import Webpack from '../../src/tasks/webpack' import { fork } from 'child_process' import EventEmitter from 'events' import winston, { Logger } from 'winston' -const logger = (winston as unknown) as Logger +const logger = winston as unknown as Logger jest.mock('child_process', () => ({ fork: jest.fn(() => { @@ -26,7 +25,7 @@ describe('webpack', () => { describe('development', () => { it('should call webpack cli with correct arguments', async () => { const configPath = 'webpack.config.js' - const task = new DevelopmentWebpack(logger, { configPath }) + const task = new Webpack(logger, 'Webpack', {}, { configPath, envName: 'development' }) await task.run() expect(fork).toBeCalledWith( @@ -40,7 +39,7 @@ describe('webpack', () => { describe('production', () => { it('should call webpack cli with correct arguments', async () => { const configPath = 'webpack.config.js' - const task = new ProductionWebpack(logger, { configPath }) + const task = new Webpack(logger, 'Webpack', {}, { configPath, envName: 'production' }) await task.run() expect(fork).toBeCalledWith( @@ -57,7 +56,7 @@ describe('webpack', () => { process.allowedNodeEnvironmentFlags = { has: jest.fn(() => true) } as any const configPath = 'webpack.config.js' - const task = new ProductionWebpack(logger, { configPath }) + const task = new Webpack(logger, 'Webpack', {}, { configPath, envName: 'production' }) await task.run() expect(mockedFork.mock.calls[0][2]?.execArgv).toEqual( @@ -70,7 +69,7 @@ describe('webpack', () => { process.allowedNodeEnvironmentFlags = { has: jest.fn(() => false) } as any const configPath = 'webpack.config.js' - const task = new ProductionWebpack(logger, { configPath }) + const task = new Webpack(logger, 'Webpack', {}, { configPath, envName: 'production' }) await task.run() expect(mockedFork.mock.calls[0][2]?.execArgv).toEqual( diff --git a/plugins/webpack/tsconfig.json b/plugins/webpack/tsconfig.json index d61291136..e868bd289 100644 --- a/plugins/webpack/tsconfig.json +++ b/plugins/webpack/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "../../tsconfig.settings.json", - "include": ["src/**/*"], + "include": [ + "src/**/*" + ], "compilerOptions": { "outDir": "lib", "rootDir": "src" @@ -10,7 +12,10 @@ "path": "../../lib/logger" }, { - "path": "../../lib/types" + "path": "../../lib/base" + }, + { + "path": "../../lib/schemas" } ] } diff --git a/readme.md b/readme.md index 0c80d8b30..e458d2ab3 100644 --- a/readme.md +++ b/readme.md @@ -6,15 +6,11 @@ Tool Kit is modern developer tooling for FT.com repositories. It's fully modular, allowing repos that need different tooling to install separate plugins that work consistently together. -Tool Kit has been created to enable the FT.com development workflow and only handles common tooling use cases that are required for most apps to work. Tool Kit sets up the minimal configuration for third party packages to run. +Tool Kit only handles common tooling use cases that are required for most apps to work. Tool Kit sets up the minimal configuration for third party packages to run. -Your application does not need to use Tool Kit for all of its tooling and tooling not supported by Tool Kit can be configured directly in your application. +Your repo does not need to use Tool Kit for all of its tooling, and tooling not supported by Tool Kit can be configured directly in your repo. -Tool Kit follows a set of [principles](./docs/tool-kit-principles.md) that guide its development. - -## Installing and using Tool Kit - -For general questions about using Tool Kit please see the [Tool Kit FAQs](./docs/faq.md). +## Installing Tool Kit ### Interactive installation & migration @@ -26,8 +22,12 @@ npm init @dotcom-tool-kit@latest See [the migration guide](./docs/migrating-to-tool-kit.md) for a full explanation of what this script does. +
+ ### Installing and configuring manually + + Install the core of Tool Kit as a `devDependency`: ```sh @@ -48,21 +48,15 @@ plugins: - '@dotcom-tool-kit/jest' ``` -Every time you change your `.toolkitrc.yml`, e.g. adding or removing a plugin, you should tell Tool Kit to install configuration files in your repository: +Every time you change your `.toolkitrc.yml`, e.g. adding or removing a plugin or [configuring](#configuring-tool-kit) a hook, you should tell Tool Kit to install configuration files in your repository: ```sh npx dotcom-tool-kit --install ``` -### Updating existing Tool Kit configuration - -If your project already has a `.toolkitrc.yml` file you can add a new plugin to it by following the steps in the README for that plugin. - -At any time you can run `npx dotcom-tool-kit --help` to review the full list of hooks and tasks configured in your project. - -You can see the list of all available hooks and tasks provided by Tool Kit in the FAQs. +
-### Running Tool Kit +## Running Tool Kit You don't run Tool Kit directly; you run plugin tasks using things like npm scripts, automatically configured in your `package.json` by Tool Kit. With the `npm` and `jest` plugins installed, Jest tests are run with the npm `test` script: @@ -76,45 +70,18 @@ At any time, you can run `--help` to see what plugins you have installed, what c npx dotcom-tool-kit --help ``` -## How Tool Kit works - -### Plugins - - - -Tool Kit is a fully modular set of developer tooling. Not every project requires the same tooling, so to make sure different projects only have to install and configure what they need, Tool Kit is made up of several **plugins** that you can install separately to provide different groups of functionality, like [the `npm` plugin](plugins/npm), which lets Tool Kit manage things like `package.json` scripts. - -This means a project that uses Jest for its tests can install [the `jest` plugin](plugins/jest), and a project using Mocha can install [the `mocha` plugin](plugins/mocha), and be able to run them consistently anywhere they're needed, e.g. the `npm run test` script. Plugins can depend on other plugins, so we can also publish plugins like `frontend-app` that bundle up most of the tooling you'll need into a single package. - -And if there's something you want to use in your repo that's not yet supported by Tool Kit, you can write a [custom plugin](docs/custom-plugins.md) so it can work consistently with any officially-supported tooling. - -Plugins provide **tasks**, which provide the code for running external tooling, and **hooks**, which manage configuration files in your repo that will be running tooling. - -### Tasks +## Core concepts -A **task** is a lightweight abstraction for running some tooling from outside of Tool Kit. Tasks are written in TypeScript, so we can make use of modern Javascript-based tooling and libraries, easily provide structured logging and actionable error messages, and debug them more easily than things like Bash scripts. +- Integration with tooling is grouped into modular **Plugins** that are installed separately. +- Tool Kit abstracts running other tooling with **Tasks**, written in Typescript. +- When running Tool Kit, you run **Commands**, which you (or a plugin) configure to run one or more Tasks. +- **Hooks** manage configuration files in your repository to run Commands from tooling such as npm scripts or CircleCI jobs. -An example of a task is `JestLocal` from the `jest` plugin, which abstracts running Jest tests in a local development environment. Some tasks support [configuration](#configuration). This doesn't replace any native configuration that tooling might have (like a `jest.config.js`). +The [concepts](./docs/concepts.md) document has more details about how these work together. -### Hooks +## Configuring Tool Kit - - -A **hook** is the glue between configuration in your repo that will be running Tool Kit and the tasks themselves. Things like scripts in `package.json` or jobs in your CircleCI config can be automatically managed and kept consistent by hooks. - -For example, the `test:local` hook in the `npm` plugin ensures the `test` script is defined in `package.json` to run `dotcom-tool-kit test:local`, then any tasks that are configured to run on `test:local` will be run when you run `npm run test`. - -Plugins can set a default hook for their tasks to run on; for example, the `JestLocal` task [runs by default on the `test:local` hook](./plugins/jest/.toolkitrc.yml#L2). If you've got multiple tasks trying to run on the same hook by default, you'll need to [configure which you want to run](./docs/resolving-hook-conflicts.md). - - - -Hooks are there to be **installed** in your repository. Hook classes contain an `install` method that updates the relevant configuration files to run that hook. This `install` method is called when you run `npx dotcom-tool-kit --install`. This lets Tool Kit plugins automatically manage files like `package.json` or `.circleci/config.yml`. Any changes made by hook installation should be committed. - -When Tool Kit starts up, it checks whether the hooks in your plugins are correctly installed, and will print an error if they're not. This prevents repos from getting out of sync with what Tool Kit expects, ensuring repos are fully consistent and controlled by Tool Kit plugins. - -### Configuration - -The `.toolkitrc.yml` file in your repo determines everything about how Tool Kit runs in that repo. It controls what plugins are included (which determine what hooks and tasks are available), gives you fine-grained control over what tasks are running on what hooks, and lets you provide options to plugins. +The `.toolkitrc.yml` file in your repo determines everything about how Tool Kit runs in that repo. It controls what plugins are included (which determine what hooks and tasks are available), gives you fine-grained control over what tasks are running on what commands, and lets you provide options to plugins, tasks, and hooks. An example `.toolkitrc.yml` might look like: @@ -122,39 +89,43 @@ An example `.toolkitrc.yml` might look like: plugins: # provides the test:local hooks - '@dotcom-tool-kit/npm' - # provides the JestLocal task + # provides the Jest task - '@dotcom-tool-kit/jest' # provides the Eslint task - '@dotcom-tool-kit/eslint' -hooks: +commands: # run both Jest and ESlint when running `npm run test` # required to resolve the conflict between their defaults test:local: - Eslint - - JestLocal + - Jest + # options for tasks can be set on a per-command basis + test:e2e: + - Jest + configFile: jest.e2e.config.js options: # ESlint plugin needs to know which files to run ESlint on. # there's a default setting, but your repo might need something else - '@dotcom-tool-kit/eslint': - files: - - server/**/*.js + tasks: + Eslint + files: + - server/**/*.js + # instruct the hook that manages your repo's `package.json` to + # install a command to run `test:e2e` as an npm script + hooks: + - PackageJson: + scripts: + test-e2e: 'test:e2e' ``` The options available for each plugin are documented in their readmes. If the tooling that a plugin is using has its own method of configuration (like `.eslintrc`, `.babelrc`, `jest.config.js`, `webpack.config.js`), Tool Kit options aren't used for that; they're not merged with that config and don't replace it. Tool Kit options are only for things that need to be known to run the tooling in the first place, or where tooling doesn't provide its own configuration. -## Contributing - -Tool Kit is organised as a monorepo with all the different plugins and libraries stored in a single repository. This allows us to quickly investigate and make changes across the whole codebase, as well as making installation easier by sharing dependencies. See the [developer documentation](./docs/developing-tool-kit.md) for a full explanation of the internal architecture of Tool Kit. - -Release versions are not kept in sync between the packages, as we do not want to have to a major version bump for every package whenever we release a breaking change for a single package. - -We use [release-please](https://github.com/googleapis/release-please) to manage releases and versioning. Every time we make a merge to main, release-please checks which packages have been changed, and creates a PR to make new releases for them. It uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) standard to determine whether updates require a patch, minor, or major version bump, and we use [commitlint](https://commitlint.js.org) to enforce the standard in all of our commits. - -This means you should make an effort to think carefully about whether the changes you're making are a new feature or bug fix, and whether they contain any breaking changes. This might seem burdensome at first but it's good practice to make sure you can predict whether other teams' builds are going to break because of your code changes! If your commit will only affect a single package then please also include the name of the package (without the `@dotcom-tool-kit` namespace) in the scope of your commit message, as this makes it easier to see where changes are being made just by a quick glance at the git log. For example, a commit message for a new feature for the `circleci` plugin might look like: -``` -feat(circleci): add support for nightly workflows -``` +## More documentation -Note that new plugins should be created with a version number of `0.1.0`. This indicates that the package is still in the early stages of development and could be subject to many breaking changes before it's stabilised. Committing breaking changes whilst your package is `<1.0.0` are treated as minor bumps (`0.2.0`) and both new features and bug fixes as patch bumps (`0.1.1`.) When you're ready, you can release a 1.0 of your plugin by including `Release-As: 1.0.0` in the body of the release commit. +- [Tool Kit's core concepts](./docs/concepts.md) +- [Extending Tool Kit with new tooling](./docs/extending-tool-kit.md) +- [How to resolve conflicts between plugins](./docs/resolving-plugin-conflicts.md) +- [Developing Tool Kit](./docs/developing-tool-kit.md) +- [Principles to follow for Tool Kit](./docs/tool-kit-principles.md) diff --git a/release-please-config.json b/release-please-config.json index aed5a069f..b444ce935 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -7,13 +7,17 @@ "packages": { "core/cli": {}, "core/create": {}, + "lib/base": {}, + "lib/config": {}, + "lib/conflict": {}, "lib/doppler": {}, "lib/error": {}, "lib/logger": {}, "lib/options": {}, - "lib/package-json-hook": {}, + "lib/plugin": {}, + "lib/schemas": {}, "lib/state": {}, - "lib/types": {}, + "lib/validated": {}, "lib/vault": {}, "lib/wait-for-ok": {}, "orb": { @@ -43,6 +47,7 @@ "plugins/node": {}, "plugins/nodemon": {}, "plugins/npm": {}, + "plugins/package-json-hook": {}, "plugins/pa11y": {}, "plugins/prettier": {}, "plugins/serverless": {}, diff --git a/scripts/create-plugin.js b/scripts/create-plugin.js index 4f498a4e8..f33e804ed 100755 --- a/scripts/create-plugin.js +++ b/scripts/create-plugin.js @@ -17,7 +17,7 @@ console.log('📦 initialising package') execSync('npm init -y --scope @dotcom-tool-kit') console.log('📥 installing dependencies') -execSync('npm install @dotcom-tool-kit/types') +execSync('npm install @dotcom-tool-kit/base') console.log('🔣 adding metadata to package.json') @@ -35,7 +35,7 @@ pkg.homepage = `https://github.com/financial-times/dotcom-tool-kit/tree/main/${d pkg.author = 'FT.com Platforms Team ' pkg.files = ['/lib', '.toolkitrc.yml'] pkg.engines = { - node: '16.x || 18.x', + node: '18.x || 20.x', npm: '7.x || 8.x || 9.x' } pkg.peerDependencies = { @@ -53,7 +53,7 @@ const tsconfig = { }, references: [ { - path: '../../lib/types' + path: '../../lib/base' } ], include: ['src/**/*'] @@ -62,7 +62,7 @@ const tsconfig = { fs.writeFileSync('tsconfig.json', JSON.stringify(tsconfig, null, 2)) console.log('📄 adding empty toolkit config') -fs.writeFileSync('.toolkitrc.yml', '') +fs.writeFileSync('.toolkitrc.yml', 'version: 2\n') console.log('🔗 adding reference to root tsconfig') const rootTsconfig = JSON.parse(fs.readFileSync('../../tsconfig.json')) @@ -70,30 +70,4 @@ rootTsconfig.references.push({ path: directory }) fs.writeFileSync('../../tsconfig.json', JSON.stringify(rootTsconfig, null, 2)) -console.log(`🏗 scaffolding task ${camelCaseName}`) -fs.mkdirSync('src/tasks', { recursive: true }) - -fs.writeFileSync( - `src/tasks/${name}.ts`, - `import { Task } from '@dotcom-tool-kit/types' - -export default class ${camelCaseName} extends Task { - static description = '' - - async run(): Promise { - - } -}` -) - -fs.writeFileSync( - 'src/index.ts', - `import ${camelCaseName} from './tasks/${name}' - -export const tasks = [ - ${camelCaseName} -] -` -) - console.log('🌊 byeee~') diff --git a/scripts/generate-and-commit-docs.sh b/scripts/generate-and-commit-docs.sh new file mode 100755 index 000000000..040446e57 --- /dev/null +++ b/scripts/generate-and-commit-docs.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -euo pipefail + +if git diff --quiet --exit-code && git diff --cached --quiet --exit-code; then + HAS_CHANGES="no" +else + HAS_CHANGES="yes" +fi + +if [ "$HAS_CHANGES" == "yes" ]; then + git stash --staged --quiet +fi + +npm run build +node ./scripts/generate-docs.js + +if ! git diff --quiet --exit-code; then + git commit -m 'docs: automatically regenerate schema docs' plugins/\*/readme.md + + echo '' + echo -e $'\e[31m!! automatically generated documentation has been regenerated and committed. please push again\e[0m' + echo '' + + if [ "$HAS_CHANGES" == "yes" ]; then + git stash pop + fi + + exit 1 +fi + +if [ "$HAS_CHANGES" == "yes" ]; then + git stash pop +fi diff --git a/scripts/generate-docs.js b/scripts/generate-docs.js new file mode 100644 index 000000000..356727a38 --- /dev/null +++ b/scripts/generate-docs.js @@ -0,0 +1,136 @@ +const { loadToolKitRC } = require('../core/cli/lib/rc-file') +const { TaskSchemas } = require('../lib/schemas/lib/tasks') +const { HookSchemas } = require('../lib/schemas/lib/hooks') +const { PluginSchemas } = require('../lib/schemas/lib/plugins') +const { default: $t } = require('endent') +const logger = require('winston') +const path = require('path') +const fs = require('fs/promises') +const { convertSchemas, formatModelsAsMarkdown } = require('zod2md') + +function formatSchemas(title, schemas) { + const converted = schemas.flatMap((schema) => { + const converted = convertSchemas([schema]) + + // for more complex types, the autogenerated schema docs aren't that readable. + // we can hide them by adding this HTML commment in the description. + if (converted[0]?.description?.includes('')) { + converted[0].type = undefined + } + + return converted + }) + + return postProcessMarkdown( + formatModelsAsMarkdown(converted, { title, transformName: (name) => '`' + name + '`' }) + ) +} + +async function formatPluginSchemas(plugin) { + const rcFile = await loadToolKitRC(logger, path.join('plugins', plugin), false) + const pluginPackage = `@dotcom-tool-kit/${plugin}` + const hooks = Object.keys(rcFile.installs) + const tasks = Object.keys(rcFile.tasks) + + const tasksWithSchemas = tasks.filter((task) => TaskSchemas[task]) + const hooksWithSchemas = hooks.filter((hook) => HookSchemas[hook]) + return $t` + ${ + tasksWithSchemas.length + ? formatSchemas( + 'Tasks', + tasksWithSchemas.map((task) => ({ + name: task, + schema: TaskSchemas[task].describe((TaskSchemas[task].description ?? '') + '\n### Task options') + })) + ) + : '' + } + ${ + hooksWithSchemas.length + ? formatSchemas( + 'Hooks', + hooksWithSchemas.map((hook) => ({ + name: hook, + schema: HookSchemas[hook].describe((HookSchemas[hook].description ?? '') + '\n### Hook options') + })) + ) + : '' + } + ${ + PluginSchemas[pluginPackage] + ? formatSchemas('Plugin-wide options', [ + { + name: pluginPackage, + schema: PluginSchemas[pluginPackage] + } + ]) + : '' + } + ` +} + +const schemasToRemove = () => + new RegExp( + `#### (Task|Hook) options + +(\\| Property \\| Type \\| +\\| :------- \\| :--- \\| + +_All properties are optional._|undefined) +`, + 'g' + ) + +function postProcessMarkdown(markdown) { + return markdown + .replace(/^#/gm, '##') + .replace(/_Object containing the following properties:_\n\n/g, '') + .replaceAll(schemasToRemove(), '') +} + +const BEGIN_COMMENT = '\n' +const END_COMMENT = '' + +function replaceBetween(text, replaceWith, startMarker, endMarker) { + const startIndex = text.indexOf(startMarker) + if (startIndex === -1) { + throw new Error(`Start marker "${startMarker}" not found`) + } + + const endIndex = text.indexOf(endMarker, startIndex + startMarker.length) + if (endIndex === -1) { + throw new Error(`End marker "${endMarker}" not found after start marker "${startMarker}"`) + } + + return text.slice(0, startIndex + startMarker.length) + replaceWith + text.slice(endIndex) +} + +async function main() { + const plugins = await fs.readdir('plugins') + + return await Promise.all( + plugins.map(async (plugin) => { + const readmePath = path.join('plugins', plugin, 'readme.md') + const generatedOptionsMarkdown = await formatPluginSchemas(plugin) + + const originalReadme = await fs.readFile(readmePath, 'utf-8') + + try { + const replacedReadme = replaceBetween( + originalReadme, + generatedOptionsMarkdown, + BEGIN_COMMENT, + END_COMMENT + ) + + await fs.writeFile(readmePath, replacedReadme, 'utf-8') + console.log(`written ${readmePath}`) + } catch (e) { + console.error(`no replacement markers in ${readmePath}`) + } + }) + ) +} + +main() diff --git a/tsconfig.json b/tsconfig.json index e24cd1f51..dbc4af851 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -44,7 +44,7 @@ "path": "lib/error" }, { - "path": "lib/package-json-hook" + "path": "plugins/package-json-hook" }, { "path": "plugins/circleci" @@ -62,7 +62,19 @@ "path": "core/create" }, { - "path": "lib/types" + "path": "lib/config" + }, + { + "path": "lib/plugin" + }, + { + "path": "lib/validated" + }, + { + "path": "lib/conflict" + }, + { + "path": "lib/base" }, { "path": "plugins/prettier" @@ -111,6 +123,12 @@ }, { "path": "plugins/serverless" + }, + { + "path": "lib/schemas" + }, + { + "path": "plugins/parallel" } ] } diff --git a/types/financial-times__package-json/package.json b/types/financial-times__package-json/package.json index dba52293e..c3a0c0750 100644 --- a/types/financial-times__package-json/package.json +++ b/types/financial-times__package-json/package.json @@ -1,7 +1,7 @@ { "name": "@types/financial-times__package-json", "private": true, - "version": "1.9.0", + "version": "2.0.0-beta.0", "description": "", "types": "index.d.ts", "keywords": [],