From d78c1cd75cc275a80cb9cedb0339c08f8d3a0cd0 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Fri, 19 Jan 2024 06:41:38 -0800 Subject: [PATCH 1/6] fix(server): ensure consistency between CLI serve entrypoints regarding help and strict (#9809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR aims to fix inconsistencies across all the CLI entry points around `--help` and strict. This PR is technically breaking but I feel like most users would consider many of these things to be bug fixes (e.g. `--help` runs the server 😂). @Tobbe we can talk in more detail about all the changes. Here's a non exhaustive list of all the changes - the `api-root-path` alias was added to `@redwoodjs/api-server` to make the options the same across `yarn rw serve` and `yarn rw-server` - refactored `@redwoodjs/api-server` entrypoint to handle `--help` and yargs strict mode and; we also moved parsing into the if block cause it was running too early (during import) - for the CLI (`yarn rw ...`) yargs strict mode wasn’t exiting with exit code 1 when unknown arguments were passed; now it does and it prints to stderr in the case there is an error - for `@redwoodjs/web-server`, passing `--help` would just run the server since we were using `yargsParser` instead of `yargs`; also added strict mode here and like the others; kept the options the same between the entrypoints - updated the server-tests tests to handle all these cases - spiked out some more tests to write to ensure we're covering all the similarities and differences before refactoring everything --------- Co-authored-by: Tobbe Lundberg --- packages/api-server/dist.test.ts | 3 +- packages/api-server/src/cliHandlers.ts | 31 +- packages/api-server/src/index.ts | 52 +- packages/cli/src/commands/serve.js | 2 +- packages/cli/src/index.js | 14 +- packages/web-server/package.json | 3 +- packages/web-server/src/server.ts | 44 +- .../__snapshots__/server.test.mjs.snap | 51 ++ tasks/server-tests/jest.config.js | 2 + .../{server.test.ts => server.test.mjs} | 563 ++++++++++-------- yarn.lock | 3 +- 11 files changed, 470 insertions(+), 298 deletions(-) create mode 100644 tasks/server-tests/__snapshots__/server.test.mjs.snap rename tasks/server-tests/{server.test.ts => server.test.mjs} (50%) diff --git a/packages/api-server/dist.test.ts b/packages/api-server/dist.test.ts index 120759bd60bc..5dbc290a5937 100644 --- a/packages/api-server/dist.test.ts +++ b/packages/api-server/dist.test.ts @@ -37,6 +37,7 @@ describe('dist', () => { "apiCliOptions": { "apiRootPath": { "alias": [ + "api-root-path", "rootPath", "root-path", ], @@ -74,7 +75,7 @@ describe('dist', () => { "webCliOptions": { "apiHost": { "alias": "api-host", - "desc": "Forward requests from the apiUrl, defined in redwood.toml to this host", + "desc": "Forward requests from the apiUrl, defined in redwood.toml, to this host", "type": "string", }, "port": { diff --git a/packages/api-server/src/cliHandlers.ts b/packages/api-server/src/cliHandlers.ts index ad04725e46ee..a1fadee69b47 100644 --- a/packages/api-server/src/cliHandlers.ts +++ b/packages/api-server/src/cliHandlers.ts @@ -29,7 +29,7 @@ export const apiCliOptions = { port: { default: getConfig().api?.port || 8911, type: 'number', alias: 'p' }, socket: { type: 'string' }, apiRootPath: { - alias: ['rootPath', 'root-path'], + alias: ['api-root-path', 'rootPath', 'root-path'], default: '/', type: 'string', desc: 'Root path where your api functions are served', @@ -49,7 +49,7 @@ export const webCliOptions = { apiHost: { alias: 'api-host', type: 'string', - desc: 'Forward requests from the apiUrl, defined in redwood.toml to this host', + desc: 'Forward requests from the apiUrl, defined in redwood.toml, to this host', }, } as const @@ -128,9 +128,24 @@ export const bothServerHandler = async (options: BothServerArgs) => { export const webServerHandler = async (options: WebServerArgs) => { const { port, socket, apiHost } = options + const apiUrl = getConfig().web.apiUrl + + if (!apiHost && !isFullyQualifiedUrl(apiUrl)) { + console.error( + `${c.red('Error')}: If you don't provide ${c.magenta( + 'apiHost' + )}, ${c.magenta( + 'apiUrl' + )} needs to be a fully-qualified URL. But ${c.magenta( + 'apiUrl' + )} is ${c.yellow(apiUrl)}.` + ) + process.exitCode = 1 + return + } + const tsServer = Date.now() process.stdout.write(c.dim(c.italic('Starting Web Server...\n'))) - const apiUrl = getConfig().web.apiUrl // Construct the graphql url from apiUrl by default // But if apiGraphQLUrl is specified, use that instead const graphqlEndpoint = coerceRootPath( @@ -172,3 +187,13 @@ function coerceRootPath(path: string) { return `${prefix}${path}${suffix}` } + +function isFullyQualifiedUrl(url: string) { + try { + // eslint-disable-next-line no-new + new URL(url) + return true + } catch (e) { + return false + } +} diff --git a/packages/api-server/src/index.ts b/packages/api-server/src/index.ts index dedf60785c19..df9ab77f35b3 100644 --- a/packages/api-server/src/index.ts +++ b/packages/api-server/src/index.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node + import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' @@ -13,29 +14,38 @@ import { export * from './types' -const positionalArgs = yargs(hideBin(process.argv)).parseSync()._ - -// "bin": { -// "rw-api-server-watch": "./dist/watch.js", -// "rw-log-formatter": "./dist/logFormatter/bin.js", -// "rw-server": "./dist/index.js" -// }, - if (require.main === module) { - if (positionalArgs.includes('api') && !positionalArgs.includes('web')) { - apiServerHandler( - yargs(hideBin(process.argv)).options(apiCliOptions).parseSync() + yargs(hideBin(process.argv)) + .scriptName('rw-server') + .usage('usage: $0 ') + .strict() + + .command( + '$0', + 'Run both api and web servers', + // @ts-expect-error just passing yargs though + (yargs) => { + yargs.options(commonOptions) + }, + bothServerHandler ) - } else if ( - positionalArgs.includes('web') && - !positionalArgs.includes('api') - ) { - webServerHandler( - yargs(hideBin(process.argv)).options(webCliOptions).parseSync() + .command( + 'api', + 'Start server for serving only the api', + // @ts-expect-error just passing yargs though + (yargs) => { + yargs.options(apiCliOptions) + }, + apiServerHandler ) - } else { - bothServerHandler( - yargs(hideBin(process.argv)).options(commonOptions).parseSync() + .command( + 'web', + 'Start server for serving only the web side', + // @ts-expect-error just passing yargs though + (yargs) => { + yargs.options(webCliOptions) + }, + webServerHandler ) - } + .parse() } diff --git a/packages/cli/src/commands/serve.js b/packages/cli/src/commands/serve.js index d06a3e871a00..296ac6715871 100644 --- a/packages/cli/src/commands/serve.js +++ b/packages/cli/src/commands/serve.js @@ -122,7 +122,7 @@ export const builder = async (yargs) => { apiHost: { alias: 'api-host', type: 'string', - desc: 'Forward requests from the apiUrl, defined in redwood.toml to this host', + desc: 'Forward requests from the apiUrl, defined in redwood.toml, to this host', }, }), handler: async (argv) => { diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index d2a16cb0e867..87553fe11243 100644 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -217,10 +217,20 @@ async function runYargs() { await loadPlugins(yarg) // Run - await yarg.parse(process.argv.slice(2), {}, (_err, _argv, output) => { + await yarg.parse(process.argv.slice(2), {}, (err, _argv, output) => { + // Configuring yargs with `strict` makes it error on unknown args; + // here we're signaling that with an exit code. + if (err) { + process.exitCode = 1 + } + // Show the output that yargs was going to if there was no callback provided if (output) { - console.log(output) + if (err) { + console.error(output) + } else { + console.log(output) + } } }) } diff --git a/packages/web-server/package.json b/packages/web-server/package.json index 152c3a59401c..63dbc2e29abb 100644 --- a/packages/web-server/package.json +++ b/packages/web-server/package.json @@ -34,10 +34,9 @@ "dotenv-defaults": "5.0.2", "fast-glob": "3.3.2", "fastify": "4.24.3", - "yargs-parser": "21.1.1" + "yargs": "17.7.2" }, "devDependencies": { - "@types/yargs-parser": "21.0.3", "esbuild": "0.19.9", "typescript": "5.3.3" }, diff --git a/packages/web-server/src/server.ts b/packages/web-server/src/server.ts index dcc5ea955405..974376a83c20 100644 --- a/packages/web-server/src/server.ts +++ b/packages/web-server/src/server.ts @@ -5,19 +5,14 @@ import path from 'path' import chalk from 'chalk' import { config } from 'dotenv-defaults' import Fastify from 'fastify' -import yargsParser from 'yargs-parser' +import { hideBin } from 'yargs/helpers' +import yargs from 'yargs/yargs' import { getPaths, getConfig } from '@redwoodjs/project-config' import { redwoodFastifyWeb } from './web' import { withApiProxy } from './withApiProxy' -interface Opts { - socket?: string - port?: string - apiHost?: string -} - function isFullyQualifiedUrl(url: string) { try { // eslint-disable-next-line no-new @@ -29,22 +24,29 @@ function isFullyQualifiedUrl(url: string) { } async function serve() { - // Parse server file args - const args = yargsParser(process.argv.slice(2), { - string: ['port', 'socket', 'apiHost'], - alias: { apiHost: ['api-host'], port: ['p'] }, - }) - - const options: Opts = { - socket: args.socket, - port: args.port, - apiHost: args.apiHost, - } + const options = yargs(hideBin(process.argv)) + .scriptName('rw-web-server') + .usage('$0', 'Start server for serving only the web side') + .strict() + + .options({ + port: { + default: getConfig().web?.port || 8910, + type: 'number', + alias: 'p', + }, + socket: { type: 'string' }, + apiHost: { + alias: 'api-host', + type: 'string', + desc: 'Forward requests from the apiUrl, defined in redwood.toml, to this host', + }, + }) + .parseSync() const redwoodProjectPaths = getPaths() const redwoodConfig = getConfig() - const port = options.port ? parseInt(options.port) : redwoodConfig.web.port const apiUrl = redwoodConfig.web.apiUrl if (!options.apiHost && !isFullyQualifiedUrl(apiUrl)) { @@ -110,7 +112,7 @@ async function serve() { listenOptions = { path: options.socket } } else { listenOptions = { - port, + port: options.port, host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::', } } @@ -121,7 +123,7 @@ async function serve() { if (options.socket) { console.log(`Web server started on ${options.socket}`) } else { - console.log(`Web server started on http://localhost:${port}`) + console.log(`Web server started on http://localhost:${options.port}`) } }) diff --git a/tasks/server-tests/__snapshots__/server.test.mjs.snap b/tasks/server-tests/__snapshots__/server.test.mjs.snap new file mode 100644 index 000000000000..4db853b513af --- /dev/null +++ b/tasks/server-tests/__snapshots__/server.test.mjs.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`serve web (/Users/dom/projects/redwood/redwood/packages/web-server/dist/server.js) errors out on unknown args 1`] = ` +"rw-web-server + +Start server for serving only the web side + +Options: + --help Show help [boolean] + --version Show version number [boolean] + -p, --port [number] [default: 8910] + --socket [string] + --apiHost, --api-host Forward requests from the apiUrl, defined in + redwood.toml, to this host [string] + +Unknown arguments: foo, bar, baz +" +`; + +exports[`serve web (/Users/dom/projects/redwood/redwood/packages/web-server/dist/server.js) fails if apiHost isn't set and apiUrl isn't fully qualified 1`] = ` +"Error: If you don't provide apiHost, apiUrl needs to be a fully-qualified URL. But apiUrl is /.redwood/functions. +" +`; + +exports[`serve web ([ + '/Users/dom/projects/redwood/redwood/packages/api-server/dist/index.js', + 'web' +]) errors out on unknown args 1`] = ` +"rw-server web + +Start server for serving only the web side + +Options: + --help Show help [boolean] + --version Show version number [boolean] + -p, --port [number] [default: 8910] + --socket [string] + --apiHost, --api-host Forward requests from the apiUrl, defined in + redwood.toml, to this host [string] + +Unknown arguments: foo, bar, baz +" +`; + +exports[`serve web ([ + '/Users/dom/projects/redwood/redwood/packages/api-server/dist/index.js', + 'web' +]) fails if apiHost isn't set and apiUrl isn't fully qualified 1`] = ` +"Error: If you don't provide apiHost, apiUrl needs to be a fully-qualified URL. But apiUrl is /.redwood/functions. +" +`; diff --git a/tasks/server-tests/jest.config.js b/tasks/server-tests/jest.config.js index 6d446413e90f..609bf5d104c9 100644 --- a/tasks/server-tests/jest.config.js +++ b/tasks/server-tests/jest.config.js @@ -1,7 +1,9 @@ /** @type {import('jest').Config} */ const config = { rootDir: '.', + testMatch: ['/*.test.mjs'], testTimeout: 5_000 * 2, + transform: {}, } module.exports = config diff --git a/tasks/server-tests/server.test.ts b/tasks/server-tests/server.test.mjs similarity index 50% rename from tasks/server-tests/server.test.ts rename to tasks/server-tests/server.test.mjs index a7e34f9af9e5..7b32fb434719 100644 --- a/tasks/server-tests/server.test.ts +++ b/tasks/server-tests/server.test.mjs @@ -1,34 +1,43 @@ -const fs = require('fs') -const http = require('http') -const path = require('path') +/* eslint-disable camelcase */ -const execa = require('execa') +import http from 'node:http' +import { fileURLToPath } from 'node:url' +import { fs, path, $ } from 'zx' + +const __dirname = fileURLToPath(new URL('./', import.meta.url)) + +const FIXTURE_PATH = fileURLToPath( + new URL('./fixtures/redwood-app', import.meta.url) +) + +//////////////////////////////////////////////////////////////// // Set up RWJS_CWD. let original_RWJS_CWD beforeAll(() => { original_RWJS_CWD = process.env.RWJS_CWD - process.env.RWJS_CWD = path.join(__dirname, './fixtures/redwood-app') + process.env.RWJS_CWD = FIXTURE_PATH }) afterAll(() => { process.env.RWJS_CWD = original_RWJS_CWD }) +//////////////////////////////////////////////////////////////// // Clean up the child process after each test. -let child +let p afterEach(async () => { - if (!child) { + if (!p) { return } - child.cancel() + p.kill() // Wait for child process to terminate. try { - await child + await p } catch (e) { // Ignore the error. } @@ -37,18 +46,15 @@ afterEach(async () => { const TIMEOUT = 1_000 * 2 const commandStrings = { - '@redwoodjs/cli': `node ${path.resolve( - __dirname, - '../../packages/cli/dist/index.js' - )} serve`, - '@redwoodjs/api-server': `node ${path.resolve( + '@redwoodjs/cli': path.resolve(__dirname, '../../packages/cli/dist/index.js'), + '@redwoodjs/api-server': path.resolve( __dirname, '../../packages/api-server/dist/index.js' - )}`, - '@redwoodjs/web-server': `node ${path.resolve( + ), + '@redwoodjs/web-server': path.resolve( __dirname, '../../packages/web-server/dist/server.js' - )}`, + ), } const redwoodToml = fs.readFileSync( @@ -61,11 +67,11 @@ const { } = redwoodToml.match(/apiUrl = "(?[^"]*)/) describe.each([ - [`${commandStrings['@redwoodjs/cli']}`], - [`${commandStrings['@redwoodjs/api-server']}`], + [[commandStrings['@redwoodjs/cli'], 'serve']], + [commandStrings['@redwoodjs/api-server']], ])('serve both (%s)', (commandString) => { it('serves both sides, using the apiRootPath in redwood.toml', async () => { - child = execa.command(commandString) + p = $`yarn node ${commandString}` await new Promise((r) => setTimeout(r, TIMEOUT)) const webRes = await fetch('http://localhost:8910/about') @@ -89,7 +95,7 @@ describe.each([ it('--port changes the port', async () => { const port = 8920 - child = execa.command(`${commandString} --port ${port}`) + p = $`yarn node ${commandString} --port ${port}` await new Promise((r) => setTimeout(r, TIMEOUT)) const webRes = await fetch(`http://localhost:${port}/about`) @@ -109,14 +115,17 @@ describe.each([ expect(apiRes.status).toEqual(200) expect(apiBody).toEqual({ data: 'hello function' }) }) + + it.todo("doesn't respect api.port in redwood.toml") + it.todo('respects web.port in redwood.toml') }) describe.each([ - [`${commandStrings['@redwoodjs/cli']} api`], - [`${commandStrings['@redwoodjs/api-server']} api`], + [[commandStrings['@redwoodjs/cli'], 'serve', 'api']], + [[commandStrings['@redwoodjs/api-server'], 'api']], ])('serve api (%s)', (commandString) => { it('serves the api side', async () => { - child = execa.command(commandString) + p = $`yarn node ${commandString}` await new Promise((r) => setTimeout(r, TIMEOUT)) const res = await fetch('http://localhost:8911/hello') @@ -129,7 +138,7 @@ describe.each([ it('--port changes the port', async () => { const port = 3000 - child = execa.command(`${commandString} --port ${port}`) + p = $`yarn node ${commandString} --port ${port}` await new Promise((r) => setTimeout(r, TIMEOUT)) const res = await fetch(`http://localhost:${port}/hello`) @@ -142,7 +151,7 @@ describe.each([ it('--apiRootPath changes the prefix', async () => { const apiRootPath = '/api' - child = execa.command(`${commandString} --apiRootPath ${apiRootPath}`) + p = $`yarn node ${commandString} --apiRootPath ${apiRootPath}` await new Promise((r) => setTimeout(r, TIMEOUT)) const res = await fetch(`http://localhost:8911${apiRootPath}/hello`) @@ -151,45 +160,25 @@ describe.each([ expect(res.status).toEqual(200) expect(body).toEqual({ data: 'hello function' }) }) + + it.todo('respects api.port in redwood.toml') + it.todo("apiRootPath isn't affected by apiUrl") }) // We can't test @redwoodjs/cli here because it depends on node_modules. describe.each([ - [`${commandStrings['@redwoodjs/api-server']} web`], + [[`${commandStrings['@redwoodjs/api-server']}`, 'web']], [commandStrings['@redwoodjs/web-server']], ])('serve web (%s)', (commandString) => { - it('serves the web side', async () => { - child = execa.command(commandString) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch('http://localhost:8910/about') - const body = await res.text() - - expect(res.status).toEqual(200) - expect(body).toEqual( - fs.readFileSync( - path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), - 'utf-8' - ) - ) - }) - - it('--port changes the port', async () => { - const port = 8912 - - child = execa.command(`${commandString} --port ${port}`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const res = await fetch(`http://localhost:${port}/about`) - const body = await res.text() - - expect(res.status).toEqual(200) - expect(body).toEqual( - fs.readFileSync( - path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), - 'utf-8' - ) - ) + it("fails if apiHost isn't set and apiUrl isn't fully qualified", async () => { + try { + await $`yarn node ${commandString}` + expect(true).toEqual(false) + } catch (p) { + expect(p.exitCode).not.toEqual(0) + expect(p.stdout).toEqual('') + expect(p.stderr).toMatchSnapshot() + } }) it('--apiHost changes the upstream api url', async () => { @@ -206,9 +195,7 @@ describe.each([ server.listen(apiPort, apiHost) - child = execa.command( - `${commandString} --apiHost http://${apiHost}:${apiPort}` - ) + p = $`yarn node ${commandString} --apiHost http://${apiHost}:${apiPort}` await new Promise((r) => setTimeout(r, TIMEOUT)) const res = await fetch('http://localhost:8910/.redwood/functions/hello') @@ -220,31 +207,47 @@ describe.each([ server.close() }) - it("doesn't error out on unknown args", async () => { - child = execa.command(`${commandString} --foo --bar --baz`) + it('--port changes the port', async () => { + const port = 8912 + + p = $`yarn node ${commandString} --apiHost http://localhost:8916 --port ${port}` await new Promise((r) => setTimeout(r, TIMEOUT)) - const res = await fetch('http://localhost:8910/about') + const res = await fetch(`http://localhost:${port}/about`) const body = await res.text() expect(res.status).toEqual(200) expect(body).toEqual( - fs.readFileSync( + await fs.readFile( path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), 'utf-8' ) ) }) + + it('errors out on unknown args', async () => { + try { + await $`yarn node ${commandString} --foo --bar --baz` + expect(true).toEqual(false) + } catch (p) { + expect(p.exitCode).toEqual(1) + expect(p.stdout).toEqual('') + expect(p.stderr).toMatchSnapshot() + } + }) + + it.todo('respects web.port in redwood.toml') + it.todo("works if apiHost isn't set and apiUrl is fully qualified") + it.todo('fails if apiHost is set and apiUrl is fully qualified') }) describe('@redwoodjs/cli', () => { describe('both server CLI', () => { - const commandString = commandStrings['@redwoodjs/cli'] - it.todo('handles --socket differently') - it('has help configured', () => { - const { stdout } = execa.commandSync(`${commandString} --help`) + it('has help configured', async () => { + const { stdout } = + await $`yarn node ${commandStrings['@redwoodjs/cli']} serve --help` expect(stdout).toMatchInlineSnapshot(` "usage: rw @@ -264,45 +267,51 @@ describe('@redwoodjs/cli', () => { --socket [string] Also see the Redwood CLI Reference - (​https://redwoodjs.com/docs/cli-commands#serve​)" + (​https://redwoodjs.com/docs/cli-commands#serve​) + " `) }) it('errors out on unknown args', async () => { - const { stdout } = execa.commandSync(`${commandString} --foo --bar --baz`) - - expect(stdout).toMatchInlineSnapshot(` - "usage: rw - - Commands: - rw serve Run both api and web servers [default] - rw serve api Start server for serving only the api - rw serve web Start server for serving only the web side - - Options: - --help Show help [boolean] - --version Show version number [boolean] - --cwd Working directory to use (where \`redwood.toml\` is located) - --telemetry Whether to send anonymous usage telemetry to RedwoodJS - [boolean] - -p, --port [number] [default: 8910] - --socket [string] - - Also see the Redwood CLI Reference - (​https://redwoodjs.com/docs/cli-commands#serve​) - - Unknown arguments: foo, bar, baz" - `) + try { + await $`yarn node ${commandStrings['@redwoodjs/cli']} serve --foo --bar --baz` + expect(true).toEqual(false) + } catch (p) { + expect(p.exitCode).toEqual(1) + expect(p.stdout).toEqual('') + expect(p.stderr).toMatchInlineSnapshot(` + "usage: rw + + Commands: + rw serve Run both api and web servers [default] + rw serve api Start server for serving only the api + rw serve web Start server for serving only the web side + + Options: + --help Show help [boolean] + --version Show version number [boolean] + --cwd Working directory to use (where \`redwood.toml\` is located) + --telemetry Whether to send anonymous usage telemetry to RedwoodJS + [boolean] + -p, --port [number] [default: 8910] + --socket [string] + + Also see the Redwood CLI Reference + (​https://redwoodjs.com/docs/cli-commands#serve​) + + Unknown arguments: foo, bar, baz + " + `) + } }) }) describe('api server CLI', () => { - const commandString = `${commandStrings['@redwoodjs/cli']} api` - it.todo('handles --socket differently') it('loads dotenv files', async () => { - child = execa.command(`${commandString}`) + p = $`yarn node ${commandStrings['@redwoodjs/cli']} serve api` + await new Promise((r) => setTimeout(r, TIMEOUT)) const res = await fetch(`http://localhost:8911/env`) @@ -312,8 +321,9 @@ describe('@redwoodjs/cli', () => { expect(body).toEqual({ data: '42' }) }) - it('has help configured', () => { - const { stdout } = execa.commandSync(`${commandString} --help`) + it('has help configured', async () => { + const { stdout } = + await $`yarn node ${commandStrings['@redwoodjs/cli']} serve api --help` expect(stdout).toMatchInlineSnapshot(` "rw serve api @@ -330,42 +340,48 @@ describe('@redwoodjs/cli', () => { -p, --port [number] [default: 8911] --socket [string] --apiRootPath, --api-root-path, Root path where your api functions - --rootPath, --root-path are served [string] [default: "/"]" + --rootPath, --root-path are served [string] [default: "/"] + " `) }) it('errors out on unknown args', async () => { - const { stdout } = execa.commandSync(`${commandString} --foo --bar --baz`) - - expect(stdout).toMatchInlineSnapshot(` - "rw serve api - - Start server for serving only the api - - Options: - --help Show help [boolean] - --version Show version number [boolean] - --cwd Working directory to use (where - \`redwood.toml\` is located) - --telemetry Whether to send anonymous usage - telemetry to RedwoodJS [boolean] - -p, --port [number] [default: 8911] - --socket [string] - --apiRootPath, --api-root-path, Root path where your api functions - --rootPath, --root-path are served [string] [default: "/"] - - Unknown arguments: foo, bar, baz" - `) + try { + await $`yarn node ${commandStrings['@redwoodjs/cli']} serve api --foo --bar --baz` + expect(true).toEqual(false) + } catch (p) { + expect(p.exitCode).toEqual(1) + expect(p.stdout).toEqual('') + expect(p.stderr).toMatchInlineSnapshot(` + "rw serve api + + Start server for serving only the api + + Options: + --help Show help [boolean] + --version Show version number [boolean] + --cwd Working directory to use (where + \`redwood.toml\` is located) + --telemetry Whether to send anonymous usage + telemetry to RedwoodJS [boolean] + -p, --port [number] [default: 8911] + --socket [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + + Unknown arguments: foo, bar, baz + " + `) + } }) }) describe('web server CLI', () => { - const commandString = `${commandStrings['@redwoodjs/cli']} web` - it.todo('handles --socket differently') - it('has help configured', () => { - const { stdout } = execa.commandSync(`${commandString} --help`) + it('has help configured', async () => { + const { stdout } = + await $`yarn node ${commandStrings['@redwoodjs/cli']} serve web --help` expect(stdout).toMatchInlineSnapshot(` "rw serve web @@ -382,44 +398,49 @@ describe('@redwoodjs/cli', () => { -p, --port [number] [default: 8910] --socket [string] --apiHost, --api-host Forward requests from the apiUrl, defined in - redwood.toml to this host [string]" + redwood.toml, to this host [string] + " `) }) it('errors out on unknown args', async () => { - const { stdout } = execa.commandSync(`${commandString} --foo --bar --baz`) - - expect(stdout).toMatchInlineSnapshot(` - "rw serve web - - Start server for serving only the web side - - Options: - --help Show help [boolean] - --version Show version number [boolean] - --cwd Working directory to use (where \`redwood.toml\` is - located) - --telemetry Whether to send anonymous usage telemetry to - RedwoodJS [boolean] - -p, --port [number] [default: 8910] - --socket [string] - --apiHost, --api-host Forward requests from the apiUrl, defined in - redwood.toml to this host [string] - - Unknown arguments: foo, bar, baz" - `) + try { + await $`yarn node ${commandStrings['@redwoodjs/cli']} serve web --foo --bar --baz` + expect(true).toEqual(false) + } catch (p) { + expect(p.exitCode).toEqual(1) + expect(p.stdout).toEqual('') + expect(p.stderr).toMatchInlineSnapshot(` + "rw serve web + + Start server for serving only the web side + + Options: + --help Show help [boolean] + --version Show version number [boolean] + --cwd Working directory to use (where \`redwood.toml\` is + located) + --telemetry Whether to send anonymous usage telemetry to + RedwoodJS [boolean] + -p, --port [number] [default: 8910] + --socket [string] + --apiHost, --api-host Forward requests from the apiUrl, defined in + redwood.toml, to this host [string] + + Unknown arguments: foo, bar, baz + " + `) + } }) }) }) describe('@redwoodjs/api-server', () => { describe('both server CLI', () => { - const commandString = commandStrings['@redwoodjs/api-server'] - it('--socket changes the port', async () => { const socket = 8921 - child = execa.command(`${commandString} --socket ${socket}`) + p = $`yarn node ${commandStrings['@redwoodjs/api-server']} --socket ${socket}` await new Promise((r) => setTimeout(r, TIMEOUT)) const webRes = await fetch(`http://localhost:${socket}/about`) @@ -446,9 +467,7 @@ describe('@redwoodjs/api-server', () => { const socket = 8922 const port = 8923 - child = execa.command( - `${commandString} --socket ${socket} --port ${port}` - ) + p = $`yarn node ${commandStrings['@redwoodjs/api-server']} --socket ${socket} --port ${port}` await new Promise((r) => setTimeout(r, TIMEOUT)) const webRes = await fetch(`http://localhost:${socket}/about`) @@ -471,48 +490,60 @@ describe('@redwoodjs/api-server', () => { expect(apiBody).toEqual({ data: 'hello function' }) }) - it("doesn't have help configured", () => { - const { stdout } = execa.commandSync(`${commandString} --help`) + it("doesn't have help configured", async () => { + const { stdout } = + await $`yarn node ${commandStrings['@redwoodjs/api-server']} --help` expect(stdout).toMatchInlineSnapshot(` - "Options: - --help Show help [boolean] - --version Show version number [boolean]" - `) - }) - - it("doesn't error out on unknown args", async () => { - child = execa.command(`${commandString} --foo --bar --baz`) - await new Promise((r) => setTimeout(r, TIMEOUT)) - - const webRes = await fetch('http://localhost:8910/about') - const webBody = await webRes.text() + "usage: rw-server - expect(webRes.status).toEqual(200) - expect(webBody).toEqual( - fs.readFileSync( - path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), - 'utf-8' - ) - ) + Commands: + rw-server Run both api and web servers [default] + rw-server api Start server for serving only the api + rw-server web Start server for serving only the web side - const apiRes = await fetch( - 'http://localhost:8910/.redwood/functions/hello' - ) - const apiBody = await apiRes.json() + Options: + --help Show help [boolean] + --version Show version number [boolean] + -p, --port [number] [default: 8910] + --socket [string] + " + `) + }) - expect(apiRes.status).toEqual(200) - expect(apiBody).toEqual({ data: 'hello function' }) + it('errors out on unknown args', async () => { + try { + await $`yarn node ${commandStrings['@redwoodjs/api-server']} --foo --bar --baz` + expect(true).toEqual(false) + } catch (p) { + expect(p.exitCode).toEqual(1) + expect(p.stdout).toEqual('') + expect(p.stderr).toMatchInlineSnapshot(` + "usage: rw-server + + Commands: + rw-server Run both api and web servers [default] + rw-server api Start server for serving only the api + rw-server web Start server for serving only the web side + + Options: + --help Show help [boolean] + --version Show version number [boolean] + -p, --port [number] [default: 8910] + --socket [string] + + Unknown arguments: foo, bar, baz + " + `) + } }) }) describe('api server CLI', () => { - const commandString = `${commandStrings['@redwoodjs/api-server']} api` - it('--socket changes the port', async () => { const socket = 3001 - child = execa.command(`${commandString} --socket ${socket}`) + p = $`yarn node ${commandStrings['@redwoodjs/api-server']} api --socket ${socket}` await new Promise((r) => setTimeout(r, TIMEOUT)) const res = await fetch(`http://localhost:${socket}/hello`) @@ -526,9 +557,7 @@ describe('@redwoodjs/api-server', () => { const socket = 3002 const port = 3003 - child = execa.command( - `${commandString} --socket ${socket} --port ${port}` - ) + p = $`yarn node ${commandStrings['@redwoodjs/api-server']} api --socket ${socket} --port ${port}` await new Promise((r) => setTimeout(r, TIMEOUT)) const res = await fetch(`http://localhost:${socket}/hello`) @@ -539,7 +568,7 @@ describe('@redwoodjs/api-server', () => { }) it('--loadEnvFiles loads dotenv files', async () => { - child = execa.command(`${commandString} --loadEnvFiles`) + p = $`yarn node ${commandStrings['@redwoodjs/api-server']} api --loadEnvFiles` await new Promise((r) => setTimeout(r, TIMEOUT)) const res = await fetch(`http://localhost:8911/env`) @@ -549,35 +578,63 @@ describe('@redwoodjs/api-server', () => { expect(body).toEqual({ data: '42' }) }) - it("doesn't have help configured", () => { - const { stdout } = execa.commandSync(`${commandString} --help`) + it('has help configured', async () => { + const { stdout } = + await $`yarn node ${commandStrings['@redwoodjs/api-server']} api --help` expect(stdout).toMatchInlineSnapshot(` - "Options: - --help Show help [boolean] - --version Show version number [boolean]" - `) - }) + "rw-server api - it("doesn't error out on unknown args", async () => { - child = execa.command(`${commandString} --foo --bar --baz`) - await new Promise((r) => setTimeout(r, TIMEOUT)) + Start server for serving only the api - const res = await fetch('http://localhost:8911/hello') - const body = await res.json() + Options: + --help Show help [boolean] + --version Show version number [boolean] + -p, --port [number] [default: 8911] + --socket [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + --loadEnvFiles Load .env and .env.defaults files + [boolean] [default: false] + " + `) + }) - expect(res.status).toEqual(200) - expect(body).toEqual({ data: 'hello function' }) + it('errors out on unknown args', async () => { + try { + await $`yarn node ${commandStrings['@redwoodjs/api-server']} api --foo --bar --baz` + expect(true).toEqual(false) + } catch (p) { + expect(p.exitCode).toEqual(1) + expect(p.stdout).toEqual('') + expect(p.stderr).toMatchInlineSnapshot(` + "rw-server api + + Start server for serving only the api + + Options: + --help Show help [boolean] + --version Show version number [boolean] + -p, --port [number] [default: 8911] + --socket [string] + --apiRootPath, --api-root-path, Root path where your api functions + --rootPath, --root-path are served [string] [default: "/"] + --loadEnvFiles Load .env and .env.defaults files + [boolean] [default: false] + + Unknown arguments: foo, bar, baz + " + `) + } }) }) describe('web server CLI', () => { - const commandString = `${commandStrings['@redwoodjs/api-server']} web` - it('--socket changes the port', async () => { const socket = 8913 - child = execa.command(`${commandString} --socket ${socket}`) + p = $`yarn node ${commandStrings['@redwoodjs/api-server']} web --socket ${socket} --apiHost="http://localhost:8910"` + await new Promise((r) => setTimeout(r, TIMEOUT)) const res = await fetch(`http://localhost:${socket}/about`) @@ -596,9 +653,7 @@ describe('@redwoodjs/api-server', () => { const socket = 8914 const port = 8915 - child = execa.command( - `${commandString} --socket ${socket} --port ${port}` - ) + p = $`yarn node ${commandStrings['@redwoodjs/api-server']} web --socket ${socket} --port ${port} --apiHost="http://localhost:8910"` await new Promise((r) => setTimeout(r, TIMEOUT)) const res = await fetch(`http://localhost:${socket}/about`) @@ -613,56 +668,74 @@ describe('@redwoodjs/api-server', () => { ) }) - it("doesn't have help configured", () => { - const { stdout } = execa.commandSync(`${commandString} --help`) + it("doesn't have help configured", async () => { + const { stdout } = + await $`yarn node ${commandStrings['@redwoodjs/api-server']} web --help` expect(stdout).toMatchInlineSnapshot(` - "Options: - --help Show help [boolean] - --version Show version number [boolean]" - `) - }) + "rw-server web - it("doesn't error out on unknown args", async () => { - child = execa.command(`${commandString} --foo --bar --baz`, { - stdio: 'inherit', - }) - await new Promise((r) => setTimeout(r, TIMEOUT)) + Start server for serving only the web side - const res = await fetch('http://localhost:8910/about') - const body = await res.text() + Options: + --help Show help [boolean] + --version Show version number [boolean] + -p, --port [number] [default: 8910] + --socket [string] + --apiHost, --api-host Forward requests from the apiUrl, defined in + redwood.toml, to this host [string] + " + `) + }) - expect(res.status).toEqual(200) - expect(body).toEqual( - fs.readFileSync( - path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), - 'utf-8' - ) - ) + it('errors out on unknown args', async () => { + try { + await $`yarn node ${commandStrings['@redwoodjs/api-server']} web --foo --bar --baz` + expect(true).toEqual(false) + } catch (p) { + expect(p.exitCode).toEqual(1) + expect(p.stdout).toEqual('') + expect(p.stderr).toMatchInlineSnapshot(` + "rw-server web + + Start server for serving only the web side + + Options: + --help Show help [boolean] + --version Show version number [boolean] + -p, --port [number] [default: 8910] + --socket [string] + --apiHost, --api-host Forward requests from the apiUrl, defined in + redwood.toml, to this host [string] + + Unknown arguments: foo, bar, baz + " + `) + } }) }) }) describe('@redwoodjs/web-server', () => { - const commandString = commandStrings['@redwoodjs/web-server'] - it.todo('handles --socket differently') - // @redwoodjs/web-server doesn't have help configured in a different way than the others. - // The others output help, it's just empty. This doesn't even do that. It just runs. - it("doesn't have help configured", async () => { - child = execa.command(`${commandString} --help`) - await new Promise((r) => setTimeout(r, TIMEOUT)) + it('has help configured', async () => { + const { stdout } = + await $`yarn node ${commandStrings['@redwoodjs/web-server']} --help` - const res = await fetch('http://localhost:8910/about') - const body = await res.text() + expect(stdout).toMatchInlineSnapshot(` + "rw-web-server - expect(res.status).toEqual(200) - expect(body).toEqual( - fs.readFileSync( - path.join(__dirname, './fixtures/redwood-app/web/dist/about.html'), - 'utf-8' - ) - ) + Start server for serving only the web side + + Options: + --help Show help [boolean] + --version Show version number [boolean] + -p, --port [number] [default: 8910] + --socket [string] + --apiHost, --api-host Forward requests from the apiUrl, defined in + redwood.toml, to this host [string] + " + `) }) }) diff --git a/yarn.lock b/yarn.lock index 19c5f8e6d538..365f69fa3c67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8721,14 +8721,13 @@ __metadata: "@fastify/static": "npm:6.12.0" "@fastify/url-data": "npm:5.4.0" "@redwoodjs/project-config": "npm:6.0.7" - "@types/yargs-parser": "npm:21.0.3" chalk: "npm:4.1.2" dotenv-defaults: "npm:5.0.2" esbuild: "npm:0.19.9" fast-glob: "npm:3.3.2" fastify: "npm:4.24.3" typescript: "npm:5.3.3" - yargs-parser: "npm:21.1.1" + yargs: "npm:17.7.2" bin: rw-web-server: ./dist/server.js languageName: unknown From 4b541bef605fec83ecee9887d396a7770ca7e10b Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Sat, 20 Jan 2024 13:46:47 +0000 Subject: [PATCH 2/6] chore(api): Switch to use vitest over jest (#9853) We know that jest will likely become troublesome for us internally and in individual user projects as we move forward with our epoch agenda. This PR switches out just one package `api` to use vitest over jest for the unit tests. The majority of the changes here are simply explicitly importing vitest testing utilities. Some notable points: 1. Style of using explicit imports over globals. I personally prefer being explicit and have done that here. There is a note from vitest that when not using globals other third party testing libraries might not work (see: https://vitest.dev/guide/migration.html#globals-as-a-default). We don't use them here so we do not need to worry about that. 2. Hook execution is by default slightly different in vitest. They run hooks like `beforeEach` in parallel rather than in series like jest would (see: https://vitest.dev/guide/migration.html#hooks). I have configured vitest to behave like jest and run them in series. We are free to revisit this decision in the future - we would have to go through the usage of each hook and confirm there are no cases where running them in parallel would cause undesirable side effects. 3. In this package there were some `__mocks__` present. Vitest does not load these by default and they must be explicitly loaded via `vi.mock(...)`. See: https://vitest.dev/guide/migration.html#auto-mocking-behaviour. It appears that for this package these mocks actually served no purpose so I have removed them here anyway. 4. CLI options are slightly different between jest and vitest. Vitest has no `--colors` like jest (see: https://jestjs.io/docs/cli#--colors). I have removed our usage of this option as I don't think we will consider losing colors in non-tty environments a blocker. `maxWorkers` is a CLI option in both vitest and jest however it appears that if you wish to use it in vitest you must also specify a `minWorkers`. I have updated appropriately with a `minWorkers=1`. Specific test change: 1. I had to update the prisma client mock in `packages/api/src/cache/__tests__/cacheFindMany.test.ts`. There already exists a note within the appropriate source code that the `PrismaClientValidationError` is not available until the prisma client has been generated. I have added a mock `PrismaClientValidationError` error and the tests pass as they used to. --- .github/workflows/ci.yml | 2 +- package.json | 4 +- packages/api/__mocks__/@prisma/client.js | 1 - packages/api/__mocks__/@redwoodjs/path.js | 12 - packages/api/package.json | 8 +- .../src/__tests__/normalizeRequest.test.ts | 1 + packages/api/src/__tests__/transforms.test.ts | 2 + .../getAuthenticationContext.test.ts | 1 + .../api/src/auth/__tests__/parseJWT.test.ts | 2 + .../__tests__/base64Sha1Verifier.test.ts | 2 + .../__tests__/base64Sha256Verifier.test.ts | 2 + .../verifiers/__tests__/jwtVerifier.test.ts | 2 + .../__tests__/secretKeyVerifier.test.ts | 10 +- .../verifiers/__tests__/sha1Verifier.test.ts | 2 + .../__tests__/sha256Verifier.test.ts | 2 + .../verifiers/__tests__/skipVerifier.test.ts | 6 +- .../__tests__/timestampSchemeVerifier.test.ts | 2 + .../api/src/cache/__tests__/cache.test.ts | 2 + .../src/cache/__tests__/cacheFindMany.test.ts | 21 +- .../cache/__tests__/deleteCacheKey.test.js | 2 + .../src/cache/__tests__/disconnect.test.ts | 8 +- .../api/src/cache/__tests__/shared.test.ts | 2 + packages/api/src/logger/logger.test.ts | 1 + .../validations/__tests__/validations.test.js | 14 +- packages/api/src/webhooks/webhooks.test.ts | 5 +- packages/api/vitest.config.mts | 13 + .../api/{jest.config.js => vitest.setup.mts} | 3 +- yarn.lock | 872 +++++++++++++++++- 28 files changed, 935 insertions(+), 69 deletions(-) delete mode 100644 packages/api/__mocks__/@prisma/client.js delete mode 100644 packages/api/__mocks__/@redwoodjs/path.js create mode 100644 packages/api/vitest.config.mts rename packages/api/{jest.config.js => vitest.setup.mts} (67%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a6d830aa149..88060543a401 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,7 +134,7 @@ jobs: uses: SimenB/github-actions-cpu-cores@v2 - name: 🧪 Test - run: yarn test-ci ${{ steps.cpu-cores.outputs.count }} + run: yarn test-ci --minWorkers=1 --maxWorkers=${{ steps.cpu-cores.outputs.count }} build-lint-test-skip: needs: detect-changes diff --git a/package.json b/package.json index 6c9b2dd030b1..27c96b9ad2c6 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "release:notes": "node ./tasks/release/generateReleaseNotes.mjs", "release:triage": "node ./tasks/release/triage/triage.mjs", "smoke-tests": "node ./tasks/smoke-tests/smoke-tests.mjs", - "test": "nx run-many -t test -- --colors --maxWorkers=4", - "test-ci": "nx run-many -t test -- --colors --maxWorkers", + "test": "nx run-many -t test -- --minWorkers=1 --maxWorkers=4", + "test-ci": "nx run-many -t test", "test:k6": "tsx ./tasks/k6-test/run-k6-tests.mts", "test:types": "tstyche" }, diff --git a/packages/api/__mocks__/@prisma/client.js b/packages/api/__mocks__/@prisma/client.js deleted file mode 100644 index a75b01bde84e..000000000000 --- a/packages/api/__mocks__/@prisma/client.js +++ /dev/null @@ -1 +0,0 @@ -export const PrismaClient = class MockPrismaClient {} diff --git a/packages/api/__mocks__/@redwoodjs/path.js b/packages/api/__mocks__/@redwoodjs/path.js deleted file mode 100644 index 76922f3e9e6c..000000000000 --- a/packages/api/__mocks__/@redwoodjs/path.js +++ /dev/null @@ -1,12 +0,0 @@ -import path from 'path' - -const BASE_PATH = path.resolve(__dirname, '../../src/__tests__/fixtures') - -export const getPaths = () => ({ - base: BASE_PATH, - api: { - src: path.resolve(BASE_PATH, './api/src'), - services: path.resolve(BASE_PATH, './api/src/services'), - graphql: path.resolve(BASE_PATH, './api/src/graphql'), - }, -}) diff --git a/packages/api/package.json b/packages/api/package.json index c6f7d5ec55fb..e2e244070be9 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -28,8 +28,8 @@ "build:types": "tsc --build --verbose", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx\" --ignore dist --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", - "test": "jest src", - "test:watch": "yarn test --watch" + "test": "vitest run src", + "test:watch": "vitest watch src" }, "dependencies": { "@babel/runtime-corejs3": "7.23.6", @@ -50,12 +50,12 @@ "@types/memjs": "1", "@types/pascalcase": "1.0.3", "@types/split2": "4.2.3", - "jest": "29.7.0", "memjs": "1.3.1", "redis": "4.6.7", "split2": "4.2.0", "ts-toolbelt": "9.6.0", - "typescript": "5.3.3" + "typescript": "5.3.3", + "vitest": "1.2.1" }, "peerDependencies": { "memjs": "1.3.1", diff --git a/packages/api/src/__tests__/normalizeRequest.test.ts b/packages/api/src/__tests__/normalizeRequest.test.ts index b1938636c897..e63299e0cab1 100644 --- a/packages/api/src/__tests__/normalizeRequest.test.ts +++ b/packages/api/src/__tests__/normalizeRequest.test.ts @@ -1,5 +1,6 @@ import { Headers } from '@whatwg-node/fetch' import type { APIGatewayProxyEvent } from 'aws-lambda' +import { test, expect } from 'vitest' import { normalizeRequest } from '../transforms' diff --git a/packages/api/src/__tests__/transforms.test.ts b/packages/api/src/__tests__/transforms.test.ts index c6b52ab1abcf..1a2fad19a80f 100644 --- a/packages/api/src/__tests__/transforms.test.ts +++ b/packages/api/src/__tests__/transforms.test.ts @@ -1,3 +1,5 @@ +import { describe, it, expect } from 'vitest' + import { removeNulls } from '../transforms' describe('removeNulls utility', () => { diff --git a/packages/api/src/auth/__tests__/getAuthenticationContext.test.ts b/packages/api/src/auth/__tests__/getAuthenticationContext.test.ts index 727c2460415e..cfbebafd8c73 100644 --- a/packages/api/src/auth/__tests__/getAuthenticationContext.test.ts +++ b/packages/api/src/auth/__tests__/getAuthenticationContext.test.ts @@ -1,4 +1,5 @@ import type { APIGatewayProxyEvent, Context } from 'aws-lambda' +import { describe, it, expect } from 'vitest' import { getAuthenticationContext } from '../index' diff --git a/packages/api/src/auth/__tests__/parseJWT.test.ts b/packages/api/src/auth/__tests__/parseJWT.test.ts index bc67194c5c58..2a5656be487f 100644 --- a/packages/api/src/auth/__tests__/parseJWT.test.ts +++ b/packages/api/src/auth/__tests__/parseJWT.test.ts @@ -1,3 +1,5 @@ +import { describe, test, expect } from 'vitest' + import { parseJWT } from '../parseJWT' const JWT_CLAIMS: Record = { diff --git a/packages/api/src/auth/verifiers/__tests__/base64Sha1Verifier.test.ts b/packages/api/src/auth/verifiers/__tests__/base64Sha1Verifier.test.ts index 90ad19da86b0..cc91f2f7e330 100644 --- a/packages/api/src/auth/verifiers/__tests__/base64Sha1Verifier.test.ts +++ b/packages/api/src/auth/verifiers/__tests__/base64Sha1Verifier.test.ts @@ -1,3 +1,5 @@ +import { describe, test, expect } from 'vitest' + import { createVerifier, WebhookVerificationError } from '../index' const stringPayload = 'No more secrets, Marty.' diff --git a/packages/api/src/auth/verifiers/__tests__/base64Sha256Verifier.test.ts b/packages/api/src/auth/verifiers/__tests__/base64Sha256Verifier.test.ts index e14a04f083d1..893d11383357 100644 --- a/packages/api/src/auth/verifiers/__tests__/base64Sha256Verifier.test.ts +++ b/packages/api/src/auth/verifiers/__tests__/base64Sha256Verifier.test.ts @@ -1,3 +1,5 @@ +import { describe, test, expect } from 'vitest' + import { createVerifier, WebhookVerificationError } from '../index' const stringPayload = 'No more secrets, Marty.' diff --git a/packages/api/src/auth/verifiers/__tests__/jwtVerifier.test.ts b/packages/api/src/auth/verifiers/__tests__/jwtVerifier.test.ts index 93a4e476654a..985c9d37ffb9 100644 --- a/packages/api/src/auth/verifiers/__tests__/jwtVerifier.test.ts +++ b/packages/api/src/auth/verifiers/__tests__/jwtVerifier.test.ts @@ -1,3 +1,5 @@ +import { describe, test, expect } from 'vitest' + import { createVerifier, WebhookSignError, diff --git a/packages/api/src/auth/verifiers/__tests__/secretKeyVerifier.test.ts b/packages/api/src/auth/verifiers/__tests__/secretKeyVerifier.test.ts index 1008abcd20d9..1c608c1a88d9 100644 --- a/packages/api/src/auth/verifiers/__tests__/secretKeyVerifier.test.ts +++ b/packages/api/src/auth/verifiers/__tests__/secretKeyVerifier.test.ts @@ -1,3 +1,5 @@ +import { beforeEach, afterEach, describe, test, vi, expect } from 'vitest' + import { createVerifier, WebhookVerificationError } from '../index' const payload = 'No more secrets, Marty.' @@ -6,11 +8,11 @@ const secret = 'MY_VOICE_IS_MY_PASSPORT_VERIFY_ME' const { sign, verify } = createVerifier('secretKeyVerifier') beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(jest.fn()) + vi.spyOn(console, 'warn').mockImplementation(vi.fn()) }) afterEach(() => { - jest.spyOn(console, 'warn').mockRestore() + vi.spyOn(console, 'warn').mockRestore() }) describe('secretKey verifier', () => { @@ -21,10 +23,10 @@ describe('secretKey verifier', () => { }) test('it verifies that the secret and signature are identical', () => { - jest.spyOn(console, 'warn').mockImplementation(jest.fn()) + vi.spyOn(console, 'warn').mockImplementation(vi.fn()) const signature = sign({ payload, secret }) expect(verify({ payload, secret, signature })).toBeTruthy() - jest.spyOn(console, 'warn').mockRestore() + vi.spyOn(console, 'warn').mockRestore() }) test('it denies verification if the secret and signature are not the same', () => { diff --git a/packages/api/src/auth/verifiers/__tests__/sha1Verifier.test.ts b/packages/api/src/auth/verifiers/__tests__/sha1Verifier.test.ts index d183a7f11f90..a3c84c260a0b 100644 --- a/packages/api/src/auth/verifiers/__tests__/sha1Verifier.test.ts +++ b/packages/api/src/auth/verifiers/__tests__/sha1Verifier.test.ts @@ -1,3 +1,5 @@ +import { describe, test, expect } from 'vitest' + import { createVerifier, WebhookVerificationError } from '../index' const stringPayload = 'No more secrets, Marty.' diff --git a/packages/api/src/auth/verifiers/__tests__/sha256Verifier.test.ts b/packages/api/src/auth/verifiers/__tests__/sha256Verifier.test.ts index 083a0b4c3a1c..cbef6446aa01 100644 --- a/packages/api/src/auth/verifiers/__tests__/sha256Verifier.test.ts +++ b/packages/api/src/auth/verifiers/__tests__/sha256Verifier.test.ts @@ -1,3 +1,5 @@ +import { describe, expect, test } from 'vitest' + import { createVerifier, WebhookVerificationError } from '../index' const stringPayload = 'No more secrets, Marty.' diff --git a/packages/api/src/auth/verifiers/__tests__/skipVerifier.test.ts b/packages/api/src/auth/verifiers/__tests__/skipVerifier.test.ts index 7f6af5ccb6d0..d4bc2be66cf9 100644 --- a/packages/api/src/auth/verifiers/__tests__/skipVerifier.test.ts +++ b/packages/api/src/auth/verifiers/__tests__/skipVerifier.test.ts @@ -1,3 +1,5 @@ +import { beforeEach, afterEach, describe, test, expect, vi } from 'vitest' + import { createVerifier } from '../index' const payload = 'No more secrets, Marty.' @@ -6,11 +8,11 @@ const secret = 'MY_VOICE_IS_MY_PASSPORT_VERIFY_ME' const { sign, verify } = createVerifier('skipVerifier') beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(jest.fn()) + vi.spyOn(console, 'warn').mockImplementation(vi.fn()) }) afterEach(() => { - jest.spyOn(console, 'warn').mockRestore() + vi.spyOn(console, 'warn').mockRestore() }) describe('skips verification verifier', () => { diff --git a/packages/api/src/auth/verifiers/__tests__/timestampSchemeVerifier.test.ts b/packages/api/src/auth/verifiers/__tests__/timestampSchemeVerifier.test.ts index 3b46ae242d4c..9ba970b7a929 100644 --- a/packages/api/src/auth/verifiers/__tests__/timestampSchemeVerifier.test.ts +++ b/packages/api/src/auth/verifiers/__tests__/timestampSchemeVerifier.test.ts @@ -1,5 +1,7 @@ // import type { APIGatewayProxyEvent } from 'aws-lambda' +import { describe, test, expect } from 'vitest' + import { createVerifier, WebhookVerificationError } from '../index' const payload = 'No more secrets, Marty.' diff --git a/packages/api/src/cache/__tests__/cache.test.ts b/packages/api/src/cache/__tests__/cache.test.ts index c12ba794dfe9..8e784f0da4ae 100644 --- a/packages/api/src/cache/__tests__/cache.test.ts +++ b/packages/api/src/cache/__tests__/cache.test.ts @@ -1,3 +1,5 @@ +import { describe, it, expect } from 'vitest' + import InMemoryClient from '../clients/InMemoryClient' import { createCache } from '../index' diff --git a/packages/api/src/cache/__tests__/cacheFindMany.test.ts b/packages/api/src/cache/__tests__/cacheFindMany.test.ts index 497b6628dacf..0eb8008daa08 100644 --- a/packages/api/src/cache/__tests__/cacheFindMany.test.ts +++ b/packages/api/src/cache/__tests__/cacheFindMany.test.ts @@ -1,23 +1,26 @@ import { PrismaClient } from '@prisma/client' +import { describe, afterEach, it, vi, expect } from 'vitest' import InMemoryClient from '../clients/InMemoryClient' import { createCache } from '../index' -const mockFindFirst = jest.fn() -const mockFindMany = jest.fn() +const mockFindFirst = vi.fn() +const mockFindMany = vi.fn() -jest.mock('@prisma/client', () => ({ - PrismaClient: jest.fn(() => ({ +vi.mock('@prisma/client', () => ({ + PrismaClient: vi.fn(() => ({ user: { findFirst: mockFindFirst, findMany: mockFindMany, }, })), + // NOTE: This is only available after `prisma generate` has been run + PrismaClientValidationError: new Error('PrismaClientValidationError'), })) describe('cacheFindMany', () => { afterEach(() => { - jest.clearAllMocks() + vi.clearAllMocks() }) it('adds the collection to the cache based on latest updated user', async () => { @@ -33,7 +36,7 @@ describe('cacheFindMany', () => { const client = new InMemoryClient() const { cacheFindMany } = createCache(client) - const spy = jest.spyOn(client, 'set') + const spy = vi.spyOn(client, 'set') await cacheFindMany('test', PrismaClient().user) @@ -66,7 +69,7 @@ describe('cacheFindMany', () => { mockFindMany.mockImplementation(() => [user]) const { cacheFindMany } = createCache(client) - const spy = jest.spyOn(client, 'set') + const spy = vi.spyOn(client, 'set') await cacheFindMany('test', PrismaClient().user) @@ -86,8 +89,8 @@ describe('cacheFindMany', () => { mockFindFirst.mockImplementation(() => null) mockFindMany.mockImplementation(() => []) const { cacheFindMany } = createCache(client) - const getSpy = jest.spyOn(client, 'get') - const setSpy = jest.spyOn(client, 'set') + const getSpy = vi.spyOn(client, 'get') + const setSpy = vi.spyOn(client, 'set') const result = await cacheFindMany('test', PrismaClient().user) diff --git a/packages/api/src/cache/__tests__/deleteCacheKey.test.js b/packages/api/src/cache/__tests__/deleteCacheKey.test.js index a53fef6c9534..0dca2c9a6e7c 100644 --- a/packages/api/src/cache/__tests__/deleteCacheKey.test.js +++ b/packages/api/src/cache/__tests__/deleteCacheKey.test.js @@ -1,3 +1,5 @@ +import { describe, it, expect } from 'vitest' + import InMemoryClient from '../clients/InMemoryClient' import { createCache } from '../index' diff --git a/packages/api/src/cache/__tests__/disconnect.test.ts b/packages/api/src/cache/__tests__/disconnect.test.ts index c7998ffc1ed0..c5a8904d1031 100644 --- a/packages/api/src/cache/__tests__/disconnect.test.ts +++ b/packages/api/src/cache/__tests__/disconnect.test.ts @@ -1,20 +1,22 @@ +import { describe, beforeEach, it, expect, vi } from 'vitest' + import InMemoryClient from '../clients/InMemoryClient' import { CacheTimeoutError } from '../errors' import { createCache } from '../index' describe('client.disconnect', () => { beforeEach(() => { - jest.clearAllMocks() + vi.clearAllMocks() }) it('attempts to disconnect on timeout error', async () => { const client = new InMemoryClient() const { cache } = createCache(client) - const getSpy = jest.spyOn(client, 'get') + const getSpy = vi.spyOn(client, 'get') getSpy.mockImplementation(() => { throw new CacheTimeoutError() }) - const disconnectSpy = jest.spyOn(client, 'disconnect') + const disconnectSpy = vi.spyOn(client, 'disconnect') await cache('test', () => { return { bar: 'baz' } diff --git a/packages/api/src/cache/__tests__/shared.test.ts b/packages/api/src/cache/__tests__/shared.test.ts index 302d8e5d5e37..b036059457ee 100644 --- a/packages/api/src/cache/__tests__/shared.test.ts +++ b/packages/api/src/cache/__tests__/shared.test.ts @@ -1,3 +1,5 @@ +import { describe, it, expect } from 'vitest' + import { createCache, formatCacheKey, InMemoryClient } from '../index' describe('exports', () => { diff --git a/packages/api/src/logger/logger.test.ts b/packages/api/src/logger/logger.test.ts index 42a1b9ce0c12..5663f9e235f4 100644 --- a/packages/api/src/logger/logger.test.ts +++ b/packages/api/src/logger/logger.test.ts @@ -3,6 +3,7 @@ import os from 'os' import { join } from 'path' import split from 'split2' +import { describe, test, expect } from 'vitest' const pid = process.pid const hostname = os.hostname() diff --git a/packages/api/src/validations/__tests__/validations.test.js b/packages/api/src/validations/__tests__/validations.test.js index 79ffdc2e819f..a411a8c4a3d3 100644 --- a/packages/api/src/validations/__tests__/validations.test.js +++ b/packages/api/src/validations/__tests__/validations.test.js @@ -1,3 +1,5 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' + import * as ValidationErrors from '../errors' import { validate, @@ -1153,7 +1155,7 @@ describe('validate', () => { describe('validateWithSync', () => { it('runs a custom function as a validation', () => { - const validateFunction = jest.fn() + const validateFunction = vi.fn() validateWithSync(validateFunction) expect(validateFunction).toBeCalledWith() @@ -1186,7 +1188,7 @@ describe('validateWithSync', () => { describe('validateWith', () => { it('runs a custom function as a validation', () => { - const validateFunction = jest.fn() + const validateFunction = vi.fn() validateWith(validateFunction) expect(validateFunction).toBeCalledWith() @@ -1220,9 +1222,9 @@ describe('validateWith', () => { // the actual methods of an instance of the class // // mockFindFirst.mockImplementation() to change what `findFirst()` would return -const mockFindFirst = jest.fn() -jest.mock('@prisma/client', () => ({ - PrismaClient: jest.fn(() => ({ +const mockFindFirst = vi.fn() +vi.mock('@prisma/client', () => ({ + PrismaClient: vi.fn(() => ({ $transaction: async (func) => func({ user: { @@ -1309,7 +1311,7 @@ describe('validateUniqueness', () => { }) it('uses the given prisma client', async () => { - const mockFindFirstOther = jest.fn() + const mockFindFirstOther = vi.fn() mockFindFirstOther.mockImplementation(() => ({ id: 2, email: 'rob@redwoodjs.com', diff --git a/packages/api/src/webhooks/webhooks.test.ts b/packages/api/src/webhooks/webhooks.test.ts index 32cc8434765b..2e479b4d7d6c 100644 --- a/packages/api/src/webhooks/webhooks.test.ts +++ b/packages/api/src/webhooks/webhooks.test.ts @@ -1,4 +1,5 @@ import type { APIGatewayProxyEvent } from 'aws-lambda' +import { beforeEach, afterEach, describe, test, expect, vi } from 'vitest' import { signPayload, @@ -44,11 +45,11 @@ const buildEvent = ({ } beforeEach(() => { - jest.spyOn(console, 'warn').mockImplementation(jest.fn()) + vi.spyOn(console, 'warn').mockImplementation(vi.fn()) }) afterEach(() => { - jest.spyOn(console, 'warn').mockRestore() + vi.spyOn(console, 'warn').mockRestore() }) describe('webhooks', () => { diff --git a/packages/api/vitest.config.mts b/packages/api/vitest.config.mts new file mode 100644 index 000000000000..b9f8b16ad6ca --- /dev/null +++ b/packages/api/vitest.config.mts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + sequence: { + hooks: 'list', + }, + setupFiles: [ + './vitest.setup.mts' + ], + logHeapUsage: true, + }, +}) diff --git a/packages/api/jest.config.js b/packages/api/vitest.setup.mts similarity index 67% rename from packages/api/jest.config.js rename to packages/api/vitest.setup.mts index 9f2758c52f1b..05aacbc43fb7 100644 --- a/packages/api/jest.config.js +++ b/packages/api/vitest.setup.mts @@ -1,5 +1,4 @@ -module.exports = {} - +// Set the default webhook secret for all tests process.env = Object.assign(process.env, { WEBHOOK_SECRET: 'MY_VOICE_IS_MY_PASSPORT_VERIFY_ME', }) diff --git a/yarn.lock b/yarn.lock index 365f69fa3c67..168ef346f123 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2393,6 +2393,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/aix-ppc64@npm:0.19.11" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.18.20": version: 0.18.20 resolution: "@esbuild/android-arm64@npm:0.18.20" @@ -2400,6 +2407,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/android-arm64@npm:0.19.11" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/android-arm64@npm:0.19.9" @@ -2414,6 +2428,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/android-arm@npm:0.19.11" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/android-arm@npm:0.19.9" @@ -2428,6 +2449,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/android-x64@npm:0.19.11" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/android-x64@npm:0.19.9" @@ -2442,6 +2470,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/darwin-arm64@npm:0.19.11" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/darwin-arm64@npm:0.19.9" @@ -2456,6 +2491,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/darwin-x64@npm:0.19.11" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/darwin-x64@npm:0.19.9" @@ -2470,6 +2512,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/freebsd-arm64@npm:0.19.11" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/freebsd-arm64@npm:0.19.9" @@ -2484,6 +2533,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/freebsd-x64@npm:0.19.11" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/freebsd-x64@npm:0.19.9" @@ -2498,6 +2554,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/linux-arm64@npm:0.19.11" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/linux-arm64@npm:0.19.9" @@ -2512,6 +2575,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/linux-arm@npm:0.19.11" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/linux-arm@npm:0.19.9" @@ -2526,6 +2596,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/linux-ia32@npm:0.19.11" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/linux-ia32@npm:0.19.9" @@ -2540,6 +2617,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/linux-loong64@npm:0.19.11" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/linux-loong64@npm:0.19.9" @@ -2554,6 +2638,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/linux-mips64el@npm:0.19.11" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/linux-mips64el@npm:0.19.9" @@ -2568,6 +2659,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/linux-ppc64@npm:0.19.11" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/linux-ppc64@npm:0.19.9" @@ -2582,6 +2680,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/linux-riscv64@npm:0.19.11" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/linux-riscv64@npm:0.19.9" @@ -2596,6 +2701,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/linux-s390x@npm:0.19.11" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/linux-s390x@npm:0.19.9" @@ -2610,6 +2722,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/linux-x64@npm:0.19.11" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/linux-x64@npm:0.19.9" @@ -2624,6 +2743,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/netbsd-x64@npm:0.19.11" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/netbsd-x64@npm:0.19.9" @@ -2638,6 +2764,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/openbsd-x64@npm:0.19.11" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/openbsd-x64@npm:0.19.9" @@ -2652,6 +2785,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/sunos-x64@npm:0.19.11" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/sunos-x64@npm:0.19.9" @@ -2666,6 +2806,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/win32-arm64@npm:0.19.11" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/win32-arm64@npm:0.19.9" @@ -2680,6 +2827,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/win32-ia32@npm:0.19.11" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/win32-ia32@npm:0.19.9" @@ -2694,6 +2848,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.19.11": + version: 0.19.11 + resolution: "@esbuild/win32-x64@npm:0.19.11" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.19.9": version: 0.19.9 resolution: "@esbuild/win32-x64@npm:0.19.9" @@ -7393,7 +7554,6 @@ __metadata: "@whatwg-node/fetch": "npm:0.9.14" core-js: "npm:3.34.0" humanize-string: "npm:2.1.0" - jest: "npm:29.7.0" jsonwebtoken: "npm:9.0.2" memjs: "npm:1.3.1" pascalcase: "npm:1.0.0" @@ -7403,6 +7563,7 @@ __metadata: title-case: "npm:3.0.3" ts-toolbelt: "npm:9.6.0" typescript: "npm:5.3.3" + vitest: "npm:1.2.1" peerDependencies: memjs: 1.3.1 redis: 4.6.7 @@ -8783,6 +8944,97 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.9.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-android-arm64@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-android-arm64@npm:4.9.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-arm64@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-darwin-arm64@npm:4.9.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-x64@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-darwin-x64@npm:4.9.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-gnueabihf@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.9.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-gnu@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.9.5" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-musl@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.9.5" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.9.5" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-gnu@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.9.5" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-musl@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.9.5" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.9.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-ia32-msvc@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.9.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.9.5": + version: 4.9.5 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.9.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@sdl-codegen/node@npm:0.0.10": version: 0.0.10 resolution: "@sdl-codegen/node@npm:0.0.10" @@ -11629,6 +11881,60 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:1.2.1": + version: 1.2.1 + resolution: "@vitest/expect@npm:1.2.1" + dependencies: + "@vitest/spy": "npm:1.2.1" + "@vitest/utils": "npm:1.2.1" + chai: "npm:^4.3.10" + checksum: ee44ba89db92698cab9b5464ce5b7f0da57a0b4809f98545dede2af1237408ecca0a261f867dce280ad7a4cb1eca5d6c677a27e784d631554eae9ecfd19926cf + languageName: node + linkType: hard + +"@vitest/runner@npm:1.2.1": + version: 1.2.1 + resolution: "@vitest/runner@npm:1.2.1" + dependencies: + "@vitest/utils": "npm:1.2.1" + p-limit: "npm:^5.0.0" + pathe: "npm:^1.1.1" + checksum: 19f1c738eecfc27220392fda180c5087cda297893c93490a3ef7dbb1cbb0c1fc57aa4bc9f7e7d5ef1b4573b31dd277236529fb61d420b640f0024fae5a26c6f0 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:1.2.1": + version: 1.2.1 + resolution: "@vitest/snapshot@npm:1.2.1" + dependencies: + magic-string: "npm:^0.30.5" + pathe: "npm:^1.1.1" + pretty-format: "npm:^29.7.0" + checksum: c92a1291c8b8579df640acb39863a5a49dd797a68b60482868cccf780d1acda44e31e1b64e5ed6788a5274a1990192d3901243399934f31ec5eed7fe32ff4ca9 + languageName: node + linkType: hard + +"@vitest/spy@npm:1.2.1": + version: 1.2.1 + resolution: "@vitest/spy@npm:1.2.1" + dependencies: + tinyspy: "npm:^2.2.0" + checksum: 1382e3641423fe85791d9a6c82b0abac88beea53a65f01355134d22503aa723760f00f0e52807bc1ff99bd342257d3f94e83da29e0bbfc17d76ebb69403e43c6 + languageName: node + linkType: hard + +"@vitest/utils@npm:1.2.1": + version: 1.2.1 + resolution: "@vitest/utils@npm:1.2.1" + dependencies: + diff-sequences: "npm:^29.6.3" + estree-walker: "npm:^3.0.3" + loupe: "npm:^2.3.7" + pretty-format: "npm:^29.7.0" + checksum: 8943d48e0b2c6f266e4f5eab549787dc7506841e95cb498b17521a339032a99ddd3f4df8ba9844dfb9b80693fb58850f0bda74ea15d1356c644caedfc864cc37 + languageName: node + linkType: hard + "@vscode/ripgrep@npm:1.15.6": version: 1.15.6 resolution: "@vscode/ripgrep@npm:1.15.6" @@ -12321,7 +12627,7 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:8.3.0, acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1": +"acorn-walk@npm:8.3.0": version: 8.3.0 resolution: "acorn-walk@npm:8.3.0" checksum: 24346e595f507b6e704a60d35f3c5e1aa9891d4fb6a3fc3d856503ab718cc26cabb5e3e1ff0ff8da6ec03d60a8226ebdb602805a94f970e7f797ea3b8b09437f @@ -12335,7 +12641,14 @@ __metadata: languageName: node linkType: hard -"acorn@npm:8.11.2, acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": +"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.3.2": + version: 8.3.2 + resolution: "acorn-walk@npm:8.3.2" + checksum: 7e2a8dad5480df7f872569b9dccff2f3da7e65f5353686b1d6032ab9f4ddf6e3a2cb83a9b52cf50b1497fd522154dda92f0abf7153290cc79cd14721ff121e52 + languageName: node + linkType: hard + +"acorn@npm:8.11.2": version: 8.11.2 resolution: "acorn@npm:8.11.2" bin: @@ -12362,6 +12675,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.10.0, acorn@npm:^8.11.0, acorn@npm:^8.11.3, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": + version: 8.11.3 + resolution: "acorn@npm:8.11.3" + bin: + acorn: bin/acorn + checksum: 3ff155f8812e4a746fee8ecff1f227d527c4c45655bb1fad6347c3cb58e46190598217551b1500f18542d2bbe5c87120cb6927f5a074a59166fbdd9468f0a299 + languageName: node + linkType: hard + "add-stream@npm:^1.0.0": version: 1.0.0 resolution: "add-stream@npm:1.0.0" @@ -13058,6 +13380,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^1.1.0": + version: 1.1.0 + resolution: "assertion-error@npm:1.1.0" + checksum: 25456b2aa333250f01143968e02e4884a34588a8538fbbf65c91a637f1dbfb8069249133cd2f4e530f10f624d206a664e7df30207830b659e9f5298b00a4099b + languageName: node + linkType: hard + "assign-symbols@npm:^1.0.0": version: 1.0.0 resolution: "assign-symbols@npm:1.0.0" @@ -14132,6 +14461,13 @@ __metadata: languageName: node linkType: hard +"cac@npm:^6.7.14": + version: 6.7.14 + resolution: "cac@npm:6.7.14" + checksum: 4ee06aaa7bab8981f0d54e5f5f9d4adcd64058e9697563ce336d8a3878ed018ee18ebe5359b2430eceae87e0758e62ea2019c3f52ae6e211b1bd2e133856cd10 + languageName: node + linkType: hard + "cacache@npm:^12.0.2": version: 12.0.4 resolution: "cacache@npm:12.0.4" @@ -14405,6 +14741,21 @@ __metadata: languageName: node linkType: hard +"chai@npm:^4.3.10": + version: 4.4.1 + resolution: "chai@npm:4.4.1" + dependencies: + assertion-error: "npm:^1.1.0" + check-error: "npm:^1.0.3" + deep-eql: "npm:^4.1.3" + get-func-name: "npm:^2.0.2" + loupe: "npm:^2.3.6" + pathval: "npm:^1.1.1" + type-detect: "npm:^4.0.8" + checksum: 91590a8fe18bd6235dece04ccb2d5b4ecec49984b50924499bdcd7a95c02cb1fd2a689407c19bb854497bde534ef57525cfad6c7fdd2507100fd802fbc2aefbd + languageName: node + linkType: hard + "chalk@npm:4.1.0": version: 4.1.0 resolution: "chalk@npm:4.1.0" @@ -14537,6 +14888,15 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^1.0.3": + version: 1.0.3 + resolution: "check-error@npm:1.0.3" + dependencies: + get-func-name: "npm:^2.0.2" + checksum: 94aa37a7315c0e8a83d0112b5bfb5a8624f7f0f81057c73e4707729cdd8077166c6aefb3d8e2b92c63ee130d4a2ff94bad46d547e12f3238cc1d78342a973841 + languageName: node + linkType: hard + "check-more-types@npm:^2.24.0": version: 2.24.0 resolution: "check-more-types@npm:2.24.0" @@ -16235,6 +16595,15 @@ __metadata: languageName: node linkType: hard +"deep-eql@npm:^4.1.3": + version: 4.1.3 + resolution: "deep-eql@npm:4.1.3" + dependencies: + type-detect: "npm:^4.0.0" + checksum: ff34e8605d8253e1bf9fe48056e02c6f347b81d9b5df1c6650a1b0f6f847b4a86453b16dc226b34f853ef14b626e85d04e081b022e20b00cd7d54f079ce9bbdd + languageName: node + linkType: hard + "deep-equal@npm:^2.0.5": version: 2.2.1 resolution: "deep-equal@npm:2.2.1" @@ -17521,6 +17890,86 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.19.3": + version: 0.19.11 + resolution: "esbuild@npm:0.19.11" + dependencies: + "@esbuild/aix-ppc64": "npm:0.19.11" + "@esbuild/android-arm": "npm:0.19.11" + "@esbuild/android-arm64": "npm:0.19.11" + "@esbuild/android-x64": "npm:0.19.11" + "@esbuild/darwin-arm64": "npm:0.19.11" + "@esbuild/darwin-x64": "npm:0.19.11" + "@esbuild/freebsd-arm64": "npm:0.19.11" + "@esbuild/freebsd-x64": "npm:0.19.11" + "@esbuild/linux-arm": "npm:0.19.11" + "@esbuild/linux-arm64": "npm:0.19.11" + "@esbuild/linux-ia32": "npm:0.19.11" + "@esbuild/linux-loong64": "npm:0.19.11" + "@esbuild/linux-mips64el": "npm:0.19.11" + "@esbuild/linux-ppc64": "npm:0.19.11" + "@esbuild/linux-riscv64": "npm:0.19.11" + "@esbuild/linux-s390x": "npm:0.19.11" + "@esbuild/linux-x64": "npm:0.19.11" + "@esbuild/netbsd-x64": "npm:0.19.11" + "@esbuild/openbsd-x64": "npm:0.19.11" + "@esbuild/sunos-x64": "npm:0.19.11" + "@esbuild/win32-arm64": "npm:0.19.11" + "@esbuild/win32-ia32": "npm:0.19.11" + "@esbuild/win32-x64": "npm:0.19.11" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 0fd913124089e26d30ec30f73b94d4ef9607935251df3253f869106980a5d4c78aa517738c8746abe6e933262e91a77d31427ce468ed8fc7fe498a20f7f92fbc + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -17926,6 +18375,15 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d + languageName: node + linkType: hard + "esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -18059,6 +18517,23 @@ __metadata: languageName: node linkType: hard +"execa@npm:^8.0.1": + version: 8.0.1 + resolution: "execa@npm:8.0.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^8.0.1" + human-signals: "npm:^5.0.0" + is-stream: "npm:^3.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^5.1.0" + onetime: "npm:^6.0.0" + signal-exit: "npm:^4.1.0" + strip-final-newline: "npm:^3.0.0" + checksum: 2c52d8775f5bf103ce8eec9c7ab3059909ba350a5164744e9947ed14a53f51687c040a250bda833f906d1283aa8803975b84e6c8f7a7c42f99dc8ef80250d1af + languageName: node + linkType: hard + "executable@npm:^4.1.1": version: 4.1.1 resolution: "executable@npm:4.1.1" @@ -19272,6 +19747,13 @@ __metadata: languageName: node linkType: hard +"get-func-name@npm:^2.0.1, get-func-name@npm:^2.0.2": + version: 2.0.2 + resolution: "get-func-name@npm:2.0.2" + checksum: 89830fd07623fa73429a711b9daecdb304386d237c71268007f788f113505ef1d4cc2d0b9680e072c5082490aec9df5d7758bf5ac6f1c37062855e8e3dc0b9df + languageName: node + linkType: hard + "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2": version: 1.2.2 resolution: "get-intrinsic@npm:1.2.2" @@ -19368,6 +19850,13 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^8.0.1": + version: 8.0.1 + resolution: "get-stream@npm:8.0.1" + checksum: 5c2181e98202b9dae0bb4a849979291043e5892eb40312b47f0c22b9414fc9b28a3b6063d2375705eb24abc41ecf97894d9a51f64ff021511b504477b27b4290 + languageName: node + linkType: hard + "get-symbol-description@npm:^1.0.0": version: 1.0.0 resolution: "get-symbol-description@npm:1.0.0" @@ -20535,6 +21024,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^5.0.0": + version: 5.0.0 + resolution: "human-signals@npm:5.0.0" + checksum: 5a9359073fe17a8b58e5a085e9a39a950366d9f00217c4ff5878bd312e09d80f460536ea6a3f260b5943a01fe55c158d1cea3fc7bee3d0520aeef04f6d915c82 + languageName: node + linkType: hard + "humanize-ms@npm:^1.2.1": version: 1.2.1 resolution: "humanize-ms@npm:1.2.1" @@ -21446,6 +21942,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "is-stream@npm:3.0.0" + checksum: eb2f7127af02ee9aa2a0237b730e47ac2de0d4e76a4a905a50a11557f2339df5765eaea4ceb8029f1efa978586abe776908720bfcb1900c20c6ec5145f6f29d8 + languageName: node + linkType: hard + "is-string@npm:^1.0.5, is-string@npm:^1.0.7": version: 1.0.7 resolution: "is-string@npm:1.0.7" @@ -22679,7 +23182,7 @@ __metadata: languageName: node linkType: hard -"jsonc-parser@npm:3.2.0": +"jsonc-parser@npm:3.2.0, jsonc-parser@npm:^3.2.0": version: 3.2.0 resolution: "jsonc-parser@npm:3.2.0" checksum: 5a12d4d04dad381852476872a29dcee03a57439574e4181d91dca71904fcdcc5e8e4706c0a68a2c61ad9810e1e1c5806b5100d52d3e727b78f5cdc595401045b @@ -23310,6 +23813,16 @@ __metadata: languageName: node linkType: hard +"local-pkg@npm:^0.5.0": + version: 0.5.0 + resolution: "local-pkg@npm:0.5.0" + dependencies: + mlly: "npm:^1.4.2" + pkg-types: "npm:^1.0.3" + checksum: f61cbd00d7689f275558b1a45c7ff2a3ddf8472654123ed880215677b9adfa729f1081e50c27ffb415cdb9fa706fb755fec5e23cdd965be375c8059e87ff1cc9 + languageName: node + linkType: hard + "locate-path@npm:^2.0.0": version: 2.0.0 resolution: "locate-path@npm:2.0.0" @@ -23620,6 +24133,15 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^2.3.6, loupe@npm:^2.3.7": + version: 2.3.7 + resolution: "loupe@npm:2.3.7" + dependencies: + get-func-name: "npm:^2.0.1" + checksum: 71a781c8fc21527b99ed1062043f1f2bb30bdaf54fa4cf92463427e1718bc6567af2988300bc243c1f276e4f0876f29e3cbf7b58106fdc186915687456ce5bf4 + languageName: node + linkType: hard + "lower-case-first@npm:^2.0.2": version: 2.0.2 resolution: "lower-case-first@npm:2.0.2" @@ -24232,6 +24754,13 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: de9cc32be9996fd941e512248338e43407f63f6d497abe8441fa33447d922e927de54d4cc3c1a3c6d652857acd770389d5a3823f311a744132760ce2be15ccbf + languageName: node + linkType: hard + "mimic-response@npm:^1.0.0, mimic-response@npm:^1.0.1": version: 1.0.1 resolution: "mimic-response@npm:1.0.1" @@ -24937,6 +25466,18 @@ __metadata: languageName: node linkType: hard +"mlly@npm:^1.2.0, mlly@npm:^1.4.2": + version: 1.5.0 + resolution: "mlly@npm:1.5.0" + dependencies: + acorn: "npm:^8.11.3" + pathe: "npm:^1.1.2" + pkg-types: "npm:^1.0.3" + ufo: "npm:^1.3.2" + checksum: 0861d64f13e8e6f99e4897b652b553ded4d4b9e7b011d6afd7141e013b77ed9b9be0cd76e60c46c60c56cc9b8e27061165e5696179ba9f4161c24d162db7b621 + languageName: node + linkType: hard + "modify-values@npm:^1.0.1": version: 1.0.1 resolution: "modify-values@npm:1.0.1" @@ -25095,12 +25636,12 @@ __metadata: languageName: node linkType: hard -"nanoid@npm:^3.3.6": - version: 3.3.6 - resolution: "nanoid@npm:3.3.6" +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" bin: nanoid: bin/nanoid.cjs - checksum: 606b355960d0fcbe3d27924c4c52ef7d47d3b57208808ece73279420d91469b01ec1dce10fae512b6d4a8c5a5432b352b228336a8b2202a6ea68e67fa348e2ee + checksum: e3fb661aa083454f40500473bb69eedb85dc160e763150b9a2c567c7e9ff560ce028a9f833123b618a6ea742e311138b591910e795614a629029e86e180660f3 languageName: node linkType: hard @@ -25705,6 +26246,15 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^5.1.0": + version: 5.2.0 + resolution: "npm-run-path@npm:5.2.0" + dependencies: + path-key: "npm:^4.0.0" + checksum: 7963c1f98e42afebe9524a08b0881477ec145aab34f6018842a315422b25ad40e015bdee709b697571e5efda2ecfa2640ee917d92674e4de1166fa3532a211b1 + languageName: node + linkType: hard + "npmlog@npm:^6.0.0, npmlog@npm:^6.0.2": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -26075,6 +26625,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^6.0.0": + version: 6.0.0 + resolution: "onetime@npm:6.0.0" + dependencies: + mimic-fn: "npm:^4.0.0" + checksum: 4eef7c6abfef697dd4479345a4100c382d73c149d2d56170a54a07418c50816937ad09500e1ed1e79d235989d073a9bade8557122aee24f0576ecde0f392bb6c + languageName: node + linkType: hard + "open@npm:^8.0.4, open@npm:^8.0.9, open@npm:^8.4.0": version: 8.4.2 resolution: "open@npm:8.4.2" @@ -26247,6 +26806,15 @@ __metadata: languageName: node linkType: hard +"p-limit@npm:^5.0.0": + version: 5.0.0 + resolution: "p-limit@npm:5.0.0" + dependencies: + yocto-queue: "npm:^1.0.0" + checksum: 574e93b8895a26e8485eb1df7c4b58a1a6e8d8ae41b1750cc2cc440922b3d306044fc6e9a7f74578a883d46802d9db72b30f2e612690fcef838c173261b1ed83 + languageName: node + linkType: hard + "p-locate@npm:^2.0.0": version: 2.0.0 resolution: "p-locate@npm:2.0.0" @@ -26720,6 +27288,13 @@ __metadata: languageName: node linkType: hard +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 794efeef32863a65ac312f3c0b0a99f921f3e827ff63afa5cb09a377e202c262b671f7b3832a4e64731003fa94af0263713962d317b9887bd1e0c48a342efba3 + languageName: node + linkType: hard + "path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -26783,10 +27358,17 @@ __metadata: languageName: node linkType: hard -"pathe@npm:^1.1.0": +"pathe@npm:^1.1.0, pathe@npm:^1.1.1, pathe@npm:^1.1.2": + version: 1.1.2 + resolution: "pathe@npm:1.1.2" + checksum: 64ee0a4e587fb0f208d9777a6c56e4f9050039268faaaaecd50e959ef01bf847b7872785c36483fa5cdcdbdfdb31fef2ff222684d4fc21c330ab60395c681897 + languageName: node + linkType: hard + +"pathval@npm:^1.1.1": version: 1.1.1 - resolution: "pathe@npm:1.1.1" - checksum: 3ae5a0529c3415d91c3ac9133f52cffea54a0dd46892fe059f4b80faf36fd207957d4594bdc87043b65d0761b1e5728f81f46bafff3b5302da4e2e48889b8c0e + resolution: "pathval@npm:1.1.1" + checksum: f63e1bc1b33593cdf094ed6ff5c49c1c0dc5dc20a646ca9725cc7fe7cd9995002d51d5685b9b2ec6814342935748b711bafa840f84c0bb04e38ff40a335c94dc languageName: node linkType: hard @@ -26990,6 +27572,17 @@ __metadata: languageName: node linkType: hard +"pkg-types@npm:^1.0.3": + version: 1.0.3 + resolution: "pkg-types@npm:1.0.3" + dependencies: + jsonc-parser: "npm:^3.2.0" + mlly: "npm:^1.2.0" + pathe: "npm:^1.1.0" + checksum: 7f692ff2005f51b8721381caf9bdbc7f5461506ba19c34f8631660a215c8de5e6dca268f23a319dd180b8f7c47a0dc6efea14b376c485ff99e98d810b8f786c4 + languageName: node + linkType: hard + "pkg-up@npm:^3.1.0": version: 3.1.0 resolution: "pkg-up@npm:3.1.0" @@ -27422,14 +28015,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.2.14, postcss@npm:^8.4.21, postcss@npm:^8.4.24, postcss@npm:^8.4.27": - version: 8.4.31 - resolution: "postcss@npm:8.4.31" +"postcss@npm:^8.2.14, postcss@npm:^8.4.21, postcss@npm:^8.4.24, postcss@npm:^8.4.27, postcss@npm:^8.4.32": + version: 8.4.33 + resolution: "postcss@npm:8.4.33" dependencies: - nanoid: "npm:^3.3.6" + nanoid: "npm:^3.3.7" picocolors: "npm:^1.0.0" source-map-js: "npm:^1.0.2" - checksum: 748b82e6e5fc34034dcf2ae88ea3d11fd09f69b6c50ecdd3b4a875cfc7cdca435c958b211e2cb52355422ab6fccb7d8f2f2923161d7a1b281029e4a913d59acf + checksum: 16eda83458fcd8a91bece287b5920c7f57164c3ea293e6c80d0ea71ce7843007bcd8592260a5160b9a7f02693e6ac93e2495b02d8c7596d3f3f72c1447e3ba79 languageName: node linkType: hard @@ -29164,6 +29757,60 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.2.0": + version: 4.9.5 + resolution: "rollup@npm:4.9.5" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.9.5" + "@rollup/rollup-android-arm64": "npm:4.9.5" + "@rollup/rollup-darwin-arm64": "npm:4.9.5" + "@rollup/rollup-darwin-x64": "npm:4.9.5" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.9.5" + "@rollup/rollup-linux-arm64-gnu": "npm:4.9.5" + "@rollup/rollup-linux-arm64-musl": "npm:4.9.5" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.9.5" + "@rollup/rollup-linux-x64-gnu": "npm:4.9.5" + "@rollup/rollup-linux-x64-musl": "npm:4.9.5" + "@rollup/rollup-win32-arm64-msvc": "npm:4.9.5" + "@rollup/rollup-win32-ia32-msvc": "npm:4.9.5" + "@rollup/rollup-win32-x64-msvc": "npm:4.9.5" + "@types/estree": "npm:1.0.5" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 7f241ad4028f32c1300eb8391493f192f622ed7e9564f993d8f3862be32dd995c8237f4691ea76327a323ef62808495a497eabf0c8fb0c6fa6556a69653a449f + languageName: node + linkType: hard + "root-workspace-0b6124@workspace:.": version: 0.0.0-use.local resolution: "root-workspace-0b6124@workspace:." @@ -29763,6 +30410,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: 3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 + languageName: node + linkType: hard + "signal-exit@npm:3.0.7, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -29770,10 +30424,10 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": - version: 4.0.2 - resolution: "signal-exit@npm:4.0.2" - checksum: 3c36ae214f4774b4a7cbbd2d090b2864f8da4dc3f9140ba5b76f38bea7605c7aa8042adf86e48ee8a0955108421873f9b0f20281c61b8a65da4d9c1c1de4929f +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": + version: 4.1.0 + resolution: "signal-exit@npm:4.1.0" + checksum: 41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 languageName: node linkType: hard @@ -30304,6 +30958,13 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 + languageName: node + linkType: hard + "stackframe@npm:^1.3.4": version: 1.3.4 resolution: "stackframe@npm:1.3.4" @@ -30352,6 +31013,13 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^3.5.0": + version: 3.7.0 + resolution: "std-env@npm:3.7.0" + checksum: 60edf2d130a4feb7002974af3d5a5f3343558d1ccf8d9b9934d225c638606884db4a20d2fe6440a09605bca282af6b042ae8070a10490c0800d69e82e478f41e + languageName: node + linkType: hard + "stdin-discarder@npm:^0.1.0": version: 0.1.0 resolution: "stdin-discarder@npm:0.1.0" @@ -30669,6 +31337,13 @@ __metadata: languageName: node linkType: hard +"strip-final-newline@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-final-newline@npm:3.0.0" + checksum: a771a17901427bac6293fd416db7577e2bc1c34a19d38351e9d5478c3c415f523f391003b42ed475f27e33a78233035df183525395f731d3bfb8cdcbd4da08ce + languageName: node + linkType: hard + "strip-indent@npm:^3.0.0": version: 3.0.0 resolution: "strip-indent@npm:3.0.0" @@ -30701,6 +31376,15 @@ __metadata: languageName: node linkType: hard +"strip-literal@npm:^1.3.0": + version: 1.3.0 + resolution: "strip-literal@npm:1.3.0" + dependencies: + acorn: "npm:^8.10.0" + checksum: 3c0c9ee41eb346e827eede61ef288457f53df30e3e6ff8b94fa81b636933b0c13ca4ea5c97d00a10d72d04be326da99ac819f8769f0c6407ba8177c98344a916 + languageName: node + linkType: hard + "strong-log-transformer@npm:2.1.0, strong-log-transformer@npm:^2.1.0": version: 2.1.0 resolution: "strong-log-transformer@npm:2.1.0" @@ -31235,6 +31919,27 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.5.1": + version: 2.6.0 + resolution: "tinybench@npm:2.6.0" + checksum: 60ea35699bf8bac9bc8cf279fa5877ab5b335b4673dcd07bf0fbbab9d7953a02c0ccded374677213eaa13aa147f54eb75d3230139ddbeec3875829ebe73db310 + languageName: node + linkType: hard + +"tinypool@npm:^0.8.1": + version: 0.8.2 + resolution: "tinypool@npm:0.8.2" + checksum: 8998626614172fc37c394e9a14e701dc437727fc6525488a4d4fd42044a4b2b59d6f076d750cbf5c699f79c58dd4e40599ab09e2f1ae0df4b23516b98c9c3055 + languageName: node + linkType: hard + +"tinyspy@npm:^2.2.0": + version: 2.2.0 + resolution: "tinyspy@npm:2.2.0" + checksum: 8c7b70748dd8590e85d52741db79243746c15bc03c92d75c23160a762142db577e7f53e360ba7300e321b12bca5c42dd2522a8dbeec6ba3830302573dd8516bc + languageName: node + linkType: hard + "title-case@npm:3.0.3, title-case@npm:^3.0.3": version: 3.0.3 resolution: "title-case@npm:3.0.3" @@ -31713,7 +32418,7 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:4.0.8": +"type-detect@npm:4.0.8, type-detect@npm:^4.0.0, type-detect@npm:^4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" checksum: 8fb9a51d3f365a7de84ab7f73b653534b61b622aa6800aecdb0f1095a4a646d3f5eb295322127b6573db7982afcd40ab492d038cf825a42093a58b1e1353e0bd @@ -31918,6 +32623,13 @@ __metadata: languageName: node linkType: hard +"ufo@npm:^1.3.2": + version: 1.3.2 + resolution: "ufo@npm:1.3.2" + checksum: 180f3dfcdf319b54fe0272780841c93cb08a024fc2ee5f95e63285c2a3c42d8b671cd3641e9a53aafccf100cf8466aa8c040ddfa0efea1fc1968c9bfb250a661 + languageName: node + linkType: hard + "uglify-js@npm:^3.1.4, uglify-js@npm:^3.5.1, uglify-js@npm:^3.7.7": version: 3.17.4 resolution: "uglify-js@npm:3.17.4" @@ -32560,6 +33272,21 @@ __metadata: languageName: node linkType: hard +"vite-node@npm:1.2.1": + version: 1.2.1 + resolution: "vite-node@npm:1.2.1" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.3.4" + pathe: "npm:^1.1.1" + picocolors: "npm:^1.0.0" + vite: "npm:^5.0.0" + bin: + vite-node: vite-node.mjs + checksum: 5c2393129299ecbbd0716ffc1de46479f4a7afa0d043d31e3175e69ceaaf0c363c637513fe5fa1e5e1c61ab8c55d82c7004f71a846ee8ded4d434a3370b4253f + languageName: node + linkType: hard + "vite@npm:4.5.1": version: 4.5.1 resolution: "vite@npm:4.5.1" @@ -32600,6 +33327,97 @@ __metadata: languageName: node linkType: hard +"vite@npm:^5.0.0": + version: 5.0.12 + resolution: "vite@npm:5.0.12" + dependencies: + esbuild: "npm:^0.19.3" + fsevents: "npm:~2.3.3" + postcss: "npm:^8.4.32" + rollup: "npm:^4.2.0" + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: c51b8e458851943c903fddde6973e720099ef8a5f364fb107cddade59c9e90f6d9ad98b61a7419cdfa0c6374236e10bff965d0c2d9e7b1790c68b874e5e7950c + languageName: node + linkType: hard + +"vitest@npm:1.2.1": + version: 1.2.1 + resolution: "vitest@npm:1.2.1" + dependencies: + "@vitest/expect": "npm:1.2.1" + "@vitest/runner": "npm:1.2.1" + "@vitest/snapshot": "npm:1.2.1" + "@vitest/spy": "npm:1.2.1" + "@vitest/utils": "npm:1.2.1" + acorn-walk: "npm:^8.3.2" + cac: "npm:^6.7.14" + chai: "npm:^4.3.10" + debug: "npm:^4.3.4" + execa: "npm:^8.0.1" + local-pkg: "npm:^0.5.0" + magic-string: "npm:^0.30.5" + pathe: "npm:^1.1.1" + picocolors: "npm:^1.0.0" + std-env: "npm:^3.5.0" + strip-literal: "npm:^1.3.0" + tinybench: "npm:^2.5.1" + tinypool: "npm:^0.8.1" + vite: "npm:^5.0.0" + vite-node: "npm:1.2.1" + why-is-node-running: "npm:^2.2.2" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": ^1.0.0 + "@vitest/ui": ^1.0.0 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: eb1c8a6f1bd5cef85e701cbe45a11d3ebec50264ebd499d122f35928e40cd3729cdbef21b024491ba2c36fdd3122ca57e013725c67247c6cbd274c4790edccd5 + languageName: node + linkType: hard + "vm-browserify@npm:^1.0.1": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" @@ -33273,6 +34091,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.2.2": + version: 2.2.2 + resolution: "why-is-node-running@npm:2.2.2" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 805d57eb5d33f0fb4e36bae5dceda7fd8c6932c2aeb705e30003970488f1a2bc70029ee64be1a0e1531e2268b11e65606e88e5b71d667ea745e6dc48fc9014bd + languageName: node + linkType: hard + "wide-align@npm:^1.1.5": version: 1.1.5 resolution: "wide-align@npm:1.1.5" From f2282de763446441f29abd79ac4db10566154d7d Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Sat, 20 Jan 2024 14:30:21 +0000 Subject: [PATCH 3/6] chore(crwa): switch to vitest (#9855) Continuing the exploration of switching packages over to vitest. Notes: 1. The `test:e2e` required a small change to the default vitest options. We have to add `--pool=forks` (see: https://vitest.dev/config/#pool-1-0-0 for the pool option). This is because we're using zx to do things like change directories. This is not possible with the default `threads` pool. See: https://github.com/vitest-dev/vitest/issues/566 for more information related to the specific error that I encountered. --------- Co-authored-by: Dominic Saadi --- .github/workflows/ci.yml | 2 +- packages/create-redwood-app/jest.config.js | 5 ----- packages/create-redwood-app/package.json | 5 +++-- .../create-redwood-app/tests/{e2e.test.js => e2e.test.ts} | 6 +++++- .../tests/{templates.test.js => templates.test.ts} | 3 ++- packages/create-redwood-app/vitest.config.mts | 7 +++++++ yarn.lock | 1 + 7 files changed, 19 insertions(+), 10 deletions(-) delete mode 100644 packages/create-redwood-app/jest.config.js rename packages/create-redwood-app/tests/{e2e.test.js => e2e.test.ts} (94%) rename packages/create-redwood-app/tests/{templates.test.js => templates.test.ts} (98%) create mode 100644 packages/create-redwood-app/vitest.config.mts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 88060543a401..03553e30267f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -848,7 +848,7 @@ jobs: git config --global user.name "Your Name" - name: e2e test - run: yarn test e2e + run: yarn test:e2e working-directory: ./packages/create-redwood-app env: PROJECT_PATH: ${{ env.PROJECT_PATH }} diff --git a/packages/create-redwood-app/jest.config.js b/packages/create-redwood-app/jest.config.js deleted file mode 100644 index 99951968fbc0..000000000000 --- a/packages/create-redwood-app/jest.config.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {import('jest').Config} */ -export default { - testPathIgnorePatterns: ['/node_modules/', '/templates/'], - transform: {}, -} diff --git a/packages/create-redwood-app/package.json b/packages/create-redwood-app/package.json index 1ea46667c57b..73cba0ca9ed7 100644 --- a/packages/create-redwood-app/package.json +++ b/packages/create-redwood-app/package.json @@ -19,8 +19,8 @@ "build:watch": "nodemon --watch src --ignore dist,template --exec \"yarn build\"", "prepublishOnly": "NODE_ENV=production yarn build", "set-up-test-project": "node ./scripts/setUpTestProject.js", - "test": "node --experimental-vm-modules $(yarn bin jest) templates", - "test:e2e": "node --experimental-vm-modules $(yarn bin jest) e2e", + "test": "vitest run templates", + "test:e2e": "vitest --pool=forks run e2e", "ts-to-js": "yarn node ./scripts/tsToJS.js" }, "devDependencies": { @@ -47,6 +47,7 @@ "terminal-link": "2.1.1", "untildify": "4.0.0", "uuid": "9.0.1", + "vitest": "1.2.1", "yargs": "17.7.2" }, "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1" diff --git a/packages/create-redwood-app/tests/e2e.test.js b/packages/create-redwood-app/tests/e2e.test.ts similarity index 94% rename from packages/create-redwood-app/tests/e2e.test.js rename to packages/create-redwood-app/tests/e2e.test.ts index 7ddee4eace96..50ea12a89793 100644 --- a/packages/create-redwood-app/tests/e2e.test.js +++ b/packages/create-redwood-app/tests/e2e.test.ts @@ -1,7 +1,11 @@ /* eslint-env node */ +import { describe, test, expect, it } from 'vitest' import { cd, fs, $ } from 'zx' +if (!process.env.PROJECT_PATH) { + throw new Error('PROJECT_PATH environment variable is not set') +} const projectPath = await fs.realpath(process.env.PROJECT_PATH) cd(projectPath) @@ -83,7 +87,7 @@ describe('create-redwood-app', () => { await fs.rm('./redwood-app', { recursive: true, force: true }) }) - it.failing('fails on unknown options', async () => { + it.fails('fails on unknown options', async () => { try { await $`yarn create-redwood-app --unknown-options`.timeout(2500) // Fail the test if the function didn't throw. diff --git a/packages/create-redwood-app/tests/templates.test.js b/packages/create-redwood-app/tests/templates.test.ts similarity index 98% rename from packages/create-redwood-app/tests/templates.test.js rename to packages/create-redwood-app/tests/templates.test.ts index 667011a2c6f8..be70e2c81a01 100644 --- a/packages/create-redwood-app/tests/templates.test.js +++ b/packages/create-redwood-app/tests/templates.test.ts @@ -2,6 +2,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import klawSync from 'klaw-sync' +import { describe, it, expect } from 'vitest' const TS_TEMPLATE_DIR = fileURLToPath( new URL('../templates/ts', import.meta.url) @@ -192,7 +193,7 @@ describe('JS template', () => { * @returns string[] */ function getDirectoryStructure(dir) { - let fileStructure = klawSync(dir) + const fileStructure = klawSync(dir) return fileStructure .filter( diff --git a/packages/create-redwood-app/vitest.config.mts b/packages/create-redwood-app/vitest.config.mts new file mode 100644 index 000000000000..6d9f9224671e --- /dev/null +++ b/packages/create-redwood-app/vitest.config.mts @@ -0,0 +1,7 @@ +import { defineConfig, configDefaults } from 'vitest/config' + +export default defineConfig({ + test: { + exclude: [...configDefaults.exclude, 'templates/**'], + }, +}) diff --git a/yarn.lock b/yarn.lock index 168ef346f123..715a8c3665c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15992,6 +15992,7 @@ __metadata: terminal-link: "npm:2.1.1" untildify: "npm:4.0.0" uuid: "npm:9.0.1" + vitest: "npm:1.2.1" yargs: "npm:17.7.2" bin: create-redwood-app: ./dist/create-redwood-app.js From cb7223f9b96ad9078d694a08067409024afeb372 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sat, 20 Jan 2024 17:19:03 +0100 Subject: [PATCH 4/6] Fix(crwa): Exit 0 after Quit install (#9856) ![image](https://github.com/redwoodjs/redwood/assets/30793/84c6a795-fec9-41f8-8a29-fd906075f06c) It successfully completed my command (quitting the install), so should exit with a successful exit code --- packages/create-redwood-app/src/create-redwood-app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/create-redwood-app/src/create-redwood-app.js b/packages/create-redwood-app/src/create-redwood-app.js index 71817907bea5..7f53b18731e9 100644 --- a/packages/create-redwood-app/src/create-redwood-app.js +++ b/packages/create-redwood-app/src/create-redwood-app.js @@ -173,7 +173,7 @@ async function executeCompatibilityCheck(templateDir) { if (response['override-engine-error'] === 'Quit install') { recordErrorViaTelemetry('User quit after engine check error') await shutdownTelemetry() - process.exit(1) + process.exit(0) } } catch (error) { recordErrorViaTelemetry('User cancelled install at engine check error') From 9245fe73f47948dc65445bae4b88e378563d02fa Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sat, 20 Jan 2024 08:28:25 -0800 Subject: [PATCH 5/6] chore(crwa): set `REDWOOD_CI` and `REDWOOD_DISABLE_TELEMETRY` (#9857) Forgot to set `REDWOOD_CI` and `REDWOOD_DISABLE_TELEMETRY` when I added e2e tests for CRWA in https://github.com/redwoodjs/redwood/pull/9783. It may be better to just set `REDWOOD_CI` at the very top of the workflow file but I wasn't sure if we're testing for its absence sometimes like we do for `REDWOOD_DISABLE_TELEMETRY` I think. --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03553e30267f..973cc187823f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -811,6 +811,10 @@ jobs: name: 🌲 Create Redwood App runs-on: ubuntu-latest + env: + REDWOOD_CI: 1 + REDWOOD_DISABLE_TELEMETRY: 1 + steps: - uses: actions/checkout@v4 From 41ac72814873eb37e287694122c22fa562f99b74 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Sat, 20 Jan 2024 18:26:12 -0800 Subject: [PATCH 6/6] feat(server file): add `createServer` (#9845) This PR brings the server file out of experimental by implementing a `createServer` function. This is a continuation of the work started in https://github.com/redwoodjs/redwood/pull/8119. This API was designed in response to the feedback to #8119, which gave users as much control as possible by more or less ejecting the code in api-server. This resulted in users managing lot of code that really wasn't their concern. In general it didn't feel like the Redwood way. The new API still gives users control over how the server starts but encapsulates the low-level details. I've tried to make this PR as complete as possible. I feel like it's reached that state, but there's still a few things I'd like to do. In general I'd like to deduplicate all the repeated server code. - [x] bring the server file out of experimental - [x] type the `start` function - [x] figure out how to handle the graphql function - [x] double check that `yarn rw dev` works well (namely, the watching) - [x] double check that you can pass CLI args in dev and serve - [x] the `yarn rw serve` command needs start two processes instead of one with the server file - [x] double check that env vars are being loaded - [x] right now this is imported from `@redwoodojs/api-server`. long term i don't think this is the best place for it --------- Co-authored-by: Tobbe Lundberg Co-authored-by: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> --- packages/api-server/ambient.d.ts | 1 + packages/api-server/dist.test.ts | 1 + packages/api-server/package.json | 9 + .../src/__tests__/createServer.test.ts | 330 +++++++++++++++++ packages/api-server/src/cliHandlers.ts | 4 +- packages/api-server/src/createServer.ts | 345 ++++++++++++++++++ .../src => api-server/src/plugins}/graphql.ts | 61 ++-- .../api-server/src/plugins/lambdaLoader.ts | 21 +- .../src/requestHandlers/awsLambdaFastify.ts | 2 +- packages/api-server/src/watch.ts | 17 +- packages/babel-config/src/api.ts | 10 + .../default-graphql-function/code.js | 19 + .../default-graphql-function/output.js | 20 + .../evil-graphql-function/code.js | 36 ++ .../evil-graphql-function/output.js | 42 +++ .../function-graphql-function/code.js | 21 ++ .../function-graphql-function/output.js | 21 ++ .../modified-graphql-function/code.js | 36 ++ .../modified-graphql-function/output.js | 38 ++ ...in-redwood-graphql-options-extract.test.ts | 19 + ...-plugin-redwood-graphql-options-extract.ts | 69 ++++ .../experimental/setupServerFileHandler.js | 119 ------ .../experimental/templates/server.ts.template | 113 ------ packages/cli/src/commands/serve.js | 57 ++- packages/cli/src/commands/serveApiHandler.js | 41 +-- packages/cli/src/commands/serveBothHandler.js | 62 ++-- .../setup/realtime/realtimeHandler.js | 4 +- .../server-file/serverFile.js} | 13 +- .../setup/server-file/serverFileHandler.js | 62 ++++ .../server-file/templates/server.ts.template | 13 + packages/fastify/build.mjs | 1 - packages/fastify/package.json | 1 - packages/fastify/src/index.ts | 1 - packages/graphql-server/src/types.ts | 2 +- packages/web-server/src/server.ts | 4 +- yarn.lock | 9 +- 36 files changed, 1266 insertions(+), 358 deletions(-) create mode 100644 packages/api-server/ambient.d.ts create mode 100644 packages/api-server/src/__tests__/createServer.test.ts create mode 100644 packages/api-server/src/createServer.ts rename packages/{fastify/src => api-server/src/plugins}/graphql.ts (66%) create mode 100644 packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/default-graphql-function/code.js create mode 100644 packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/default-graphql-function/output.js create mode 100644 packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/evil-graphql-function/code.js create mode 100644 packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/evil-graphql-function/output.js create mode 100644 packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/function-graphql-function/code.js create mode 100644 packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/function-graphql-function/output.js create mode 100644 packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/modified-graphql-function/code.js create mode 100644 packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/modified-graphql-function/output.js create mode 100644 packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-graphql-options-extract.test.ts create mode 100644 packages/babel-config/src/plugins/babel-plugin-redwood-graphql-options-extract.ts delete mode 100644 packages/cli/src/commands/experimental/setupServerFileHandler.js delete mode 100644 packages/cli/src/commands/experimental/templates/server.ts.template rename packages/cli/src/commands/{experimental/setupServerFile.js => setup/server-file/serverFile.js} (59%) create mode 100644 packages/cli/src/commands/setup/server-file/serverFileHandler.js create mode 100644 packages/cli/src/commands/setup/server-file/templates/server.ts.template diff --git a/packages/api-server/ambient.d.ts b/packages/api-server/ambient.d.ts new file mode 100644 index 000000000000..af5f80584cd1 --- /dev/null +++ b/packages/api-server/ambient.d.ts @@ -0,0 +1 @@ +declare module 'dotenv-defaults' diff --git a/packages/api-server/dist.test.ts b/packages/api-server/dist.test.ts index 5dbc290a5937..db82efffeb01 100644 --- a/packages/api-server/dist.test.ts +++ b/packages/api-server/dist.test.ts @@ -72,6 +72,7 @@ describe('dist', () => { "type": "string", }, }, + "createServer": [Function], "webCliOptions": { "apiHost": { "alias": "api-host", diff --git a/packages/api-server/package.json b/packages/api-server/package.json index 51a199b4da1b..c35c32665b1f 100644 --- a/packages/api-server/package.json +++ b/packages/api-server/package.json @@ -61,7 +61,16 @@ "@types/yargs": "17.0.32", "aws-lambda": "1.0.7", "jest": "29.7.0", + "pino-abstract-transport": "1.1.0", "typescript": "5.3.3" }, + "peerDependencies": { + "@redwoodjs/graphql-server": "6.0.7" + }, + "peerDependenciesMeta": { + "@redwoodjs/graphql-server": { + "optional": true + } + }, "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1" } diff --git a/packages/api-server/src/__tests__/createServer.test.ts b/packages/api-server/src/__tests__/createServer.test.ts new file mode 100644 index 000000000000..5c8130322325 --- /dev/null +++ b/packages/api-server/src/__tests__/createServer.test.ts @@ -0,0 +1,330 @@ +import path from 'path' + +import pino from 'pino' +import build from 'pino-abstract-transport' + +import { getConfig } from '@redwoodjs/project-config' + +import { + createServer, + resolveOptions, + DEFAULT_CREATE_SERVER_OPTIONS, +} from '../createServer' + +// Set up RWJS_CWD. +let original_RWJS_CWD + +beforeAll(() => { + original_RWJS_CWD = process.env.RWJS_CWD + process.env.RWJS_CWD = path.join(__dirname, './fixtures/redwood-app') +}) + +afterAll(() => { + process.env.RWJS_CWD = original_RWJS_CWD +}) + +describe('createServer', () => { + // Create a server for most tests. Some that test initialization create their own + let server + + beforeAll(async () => { + server = await createServer() + }) + + afterAll(async () => { + await server?.close() + }) + + it('serves functions', async () => { + const res = await server.inject({ + method: 'GET', + url: '/hello', + }) + + expect(res.json()).toEqual({ data: 'hello function' }) + }) + + describe('warnings', () => { + let consoleWarnSpy + + beforeAll(() => { + consoleWarnSpy = jest.spyOn(console, 'warn') + }) + + afterAll(() => { + consoleWarnSpy.mockRestore() + }) + + it('warns about server.config.js', async () => { + await createServer() + + expect(consoleWarnSpy.mock.calls[0][0]).toMatchInlineSnapshot(` + " + Ignoring \`config\` and \`configureServer\` in api/server.config.js. + Migrate them to api/src/server.{ts,js}: +  + \`\`\`js title="api/src/server.{ts,js}" + // Pass your config to \`createServer\` + const server = createServer({ +  fastifyServerOptions: myFastifyConfig + }) +  + // Then inline your \`configureFastify\` logic: + server.register(myFastifyPlugin) + \`\`\` + " + `) + }) + }) + + it('`apiRootPath` prefixes all routes', async () => { + const server = await createServer({ apiRootPath: '/api' }) + + const res = await server.inject({ + method: 'GET', + url: '/api/hello', + }) + + expect(res.json()).toEqual({ data: 'hello function' }) + + await server.close() + }) + + // We use `console.log` and `.warn` to output some things. + // Meanwhile, the server gets a logger that may not output to the same place. + // The server's logger also seems to output things out of order. + // + // This should be fixed so that all logs go to the same place + describe('logs', () => { + let consoleLogSpy + let consoleWarnSpy + + beforeAll(() => { + consoleLogSpy = jest.spyOn(console, 'log').mockImplementation() + consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation() + }) + + afterAll(() => { + consoleLogSpy.mockRestore() + consoleWarnSpy.mockRestore() + }) + + it("doesn't handle logs consistently", async () => { + // Here we create a logger that outputs to an array. + const loggerLogs: string[] = [] + const stream = build(async (source) => { + for await (const obj of source) { + loggerLogs.push(obj) + } + }) + const logger = pino(stream) + + // Generate some logs. + const server = await createServer({ logger }) + const res = await server.inject({ + method: 'GET', + url: '/hello', + }) + expect(res.json()).toEqual({ data: 'hello function' }) + await server.listen({ port: 8910 }) + await server.close() + + // We expect console log to be called with `withFunctions` logs. + expect(consoleLogSpy.mock.calls[0][0]).toMatch( + /Importing Server Functions/ + ) + + const lastCallIndex = consoleLogSpy.mock.calls.length - 1 + + expect(consoleLogSpy.mock.calls[lastCallIndex][0]).toMatch(/Listening on/) + + // `console.warn` will be used if there's a `server.config.js` file. + expect(consoleWarnSpy.mock.calls[0][0]).toMatchInlineSnapshot(` + " + Ignoring \`config\` and \`configureServer\` in api/server.config.js. + Migrate them to api/src/server.{ts,js}: +  + \`\`\`js title="api/src/server.{ts,js}" + // Pass your config to \`createServer\` + const server = createServer({ +  fastifyServerOptions: myFastifyConfig + }) +  + // Then inline your \`configureFastify\` logic: + server.register(myFastifyPlugin) + \`\`\` + " + `) + + // Finally, the logger. Notice how the request/response logs come before the "server is listening..." logs. + expect(loggerLogs[0]).toMatchObject({ + reqId: 'req-1', + level: 30, + msg: 'incoming request', + req: { + hostname: 'localhost:80', + method: 'GET', + remoteAddress: '127.0.0.1', + url: '/hello', + }, + }) + expect(loggerLogs[1]).toMatchObject({ + reqId: 'req-1', + level: 30, + msg: 'request completed', + res: { + statusCode: 200, + }, + }) + + expect(loggerLogs[2]).toMatchObject({ + level: 30, + msg: 'Server listening at http://[::1]:8910', + }) + expect(loggerLogs[3]).toMatchObject({ + level: 30, + msg: 'Server listening at http://127.0.0.1:8910', + }) + }) + }) + + describe('`server.start`', () => { + it('starts the server using [api].port in redwood.toml if none is specified', async () => { + const server = await createServer() + await server.start() + + const address = server.server.address() + + if (!address || typeof address === 'string') { + throw new Error('No address or address is a string') + } + + expect(address.port).toBe(getConfig().api.port) + + await server.close() + }) + + it('the `REDWOOD_API_PORT` env var takes precedence over [api].port', async () => { + process.env.REDWOOD_API_PORT = '8920' + + const server = await createServer() + await server.start() + + const address = server.server.address() + + if (!address || typeof address === 'string') { + throw new Error('No address or address is a string') + } + + expect(address.port).toBe(+process.env.REDWOOD_API_PORT) + + await server.close() + + delete process.env.REDWOOD_API_PORT + }) + }) +}) + +describe('resolveOptions', () => { + it('nothing passed', () => { + const resolvedOptions = resolveOptions() + + expect(resolvedOptions).toEqual({ + apiRootPath: DEFAULT_CREATE_SERVER_OPTIONS.apiRootPath, + fastifyServerOptions: { + requestTimeout: + DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout, + logger: DEFAULT_CREATE_SERVER_OPTIONS.logger, + }, + port: 8911, + }) + }) + + it('ensures `apiRootPath` has slashes', () => { + const expected = '/v1/' + + expect( + resolveOptions({ + apiRootPath: 'v1', + }).apiRootPath + ).toEqual(expected) + + expect( + resolveOptions({ + apiRootPath: '/v1', + }).apiRootPath + ).toEqual(expected) + + expect( + resolveOptions({ + apiRootPath: 'v1/', + }).apiRootPath + ).toEqual(expected) + }) + + it('moves `logger` to `fastifyServerOptions.logger`', () => { + const resolvedOptions = resolveOptions({ + logger: { level: 'info' }, + }) + + expect(resolvedOptions).toMatchObject({ + fastifyServerOptions: { + logger: { level: 'info' }, + }, + }) + }) + + it('`logger` overwrites `fastifyServerOptions.logger`', () => { + const resolvedOptions = resolveOptions({ + logger: false, + fastifyServerOptions: { + // @ts-expect-error this is invalid TS but valid JS + logger: true, + }, + }) + + expect(resolvedOptions).toMatchObject({ + fastifyServerOptions: { + logger: false, + }, + }) + }) + + it('`DEFAULT_CREATE_SERVER_OPTIONS` overwrites `fastifyServerOptions.logger`', () => { + const resolvedOptions = resolveOptions({ + fastifyServerOptions: { + // @ts-expect-error this is invalid TS but valid JS + logger: true, + }, + }) + + expect(resolvedOptions).toMatchObject({ + fastifyServerOptions: { + logger: DEFAULT_CREATE_SERVER_OPTIONS.logger, + }, + }) + }) + + it('parses `--port`', () => { + expect(resolveOptions({}, ['--port', '8930']).port).toEqual(8930) + }) + + it("throws if `--port` can't be converted to an integer", () => { + expect(() => { + resolveOptions({}, ['--port', 'eight-nine-ten']) + }).toThrowErrorMatchingInlineSnapshot(`"\`port\` must be an integer"`) + }) + + it('parses `--apiRootPath`', () => { + expect(resolveOptions({}, ['--apiRootPath', 'foo']).apiRootPath).toEqual( + '/foo/' + ) + }) + + it('the `--apiRootPath` flag has precedence', () => { + expect( + resolveOptions({ apiRootPath: 'foo' }, ['--apiRootPath', 'bar']) + .apiRootPath + ).toEqual('/bar/') + }) +}) diff --git a/packages/api-server/src/cliHandlers.ts b/packages/api-server/src/cliHandlers.ts index a1fadee69b47..b6850227592c 100644 --- a/packages/api-server/src/cliHandlers.ts +++ b/packages/api-server/src/cliHandlers.ts @@ -59,7 +59,6 @@ export const apiServerHandler = async (options: ApiServerArgs) => { process.stdout.write(c.dim(c.italic('Starting API Server...\n'))) if (loadEnvFiles) { - // @ts-expect-error for some reason ts can't find the types here but can find them for other packages const { config } = await import('dotenv-defaults') config({ @@ -197,3 +196,6 @@ function isFullyQualifiedUrl(url: string) { return false } } + +// Temporarily here till we refactor server code +export { createServer } from './createServer' diff --git a/packages/api-server/src/createServer.ts b/packages/api-server/src/createServer.ts new file mode 100644 index 000000000000..93c8a8175faa --- /dev/null +++ b/packages/api-server/src/createServer.ts @@ -0,0 +1,345 @@ +import fs from 'fs' +import path from 'path' +import { parseArgs } from 'util' + +import fastifyUrlData from '@fastify/url-data' +import c from 'ansi-colors' +import { config } from 'dotenv-defaults' +import fg from 'fast-glob' +import fastify from 'fastify' +import type { + FastifyListenOptions, + FastifyServerOptions, + FastifyInstance, + HookHandlerDoneFunction, +} from 'fastify' +import fastifyRawBody from 'fastify-raw-body' + +import type { GlobalContext } from '@redwoodjs/context' +import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' +import { getConfig, getPaths } from '@redwoodjs/project-config' + +import { + loadFunctionsFromDist, + lambdaRequestHandler, +} from './plugins/lambdaLoader' + +type StartOptions = Omit + +interface Server extends FastifyInstance { + start: (options?: StartOptions) => Promise +} + +// Load .env files if they haven't already been loaded. This makes importing this file effectful: +// +// ```js +// # Loads dotenv... +// import { createServer } from '@redwoodjs/api-server' +// ``` +// +// We do it here and not in the function below so that users can access env vars before calling `createServer` +if (process.env.RWJS_CWD && !process.env.REDWOOD_ENV_FILES_LOADED) { + config({ + path: path.join(getPaths().base, '.env'), + defaults: path.join(getPaths().base, '.env.defaults'), + multiline: true, + }) +} + +export interface CreateServerOptions { + /** + * The prefix for all routes. Defaults to `/`. + */ + apiRootPath?: string + + /** + * Logger instance or options. + */ + logger?: FastifyServerOptions['logger'] + + /** + * Options for the fastify server instance. + * Omitting logger here because we move it up. + */ + fastifyServerOptions?: Omit +} + +/** + * Creates a server for api functions: + * + * ```js + * import { createServer } from '@redwoodjs/api-server' + * + * import { logger } from 'src/lib/logger' + * + async function main() { + * const server = await createServer({ + * logger, + * apiRootPath: 'api' + * }) + * + * // Configure the returned fastify instance: + * server.register(myPlugin) + * + * // When ready, start the server: + * await server.start() + * } + * + * main() + * ``` + */ +export async function createServer(options: CreateServerOptions = {}) { + const { apiRootPath, fastifyServerOptions, port } = resolveOptions(options) + + // Warn about `api/server.config.js` + const serverConfigPath = path.join( + getPaths().base, + getConfig().api.serverConfig + ) + + if (fs.existsSync(serverConfigPath)) { + console.warn( + c.yellow( + [ + '', + `Ignoring \`config\` and \`configureServer\` in api/server.config.js.`, + `Migrate them to api/src/server.{ts,js}:`, + '', + `\`\`\`js title="api/src/server.{ts,js}"`, + '// Pass your config to `createServer`', + 'const server = createServer({', + ' fastifyServerOptions: myFastifyConfig', + '})', + '', + '// Then inline your `configureFastify` logic:', + 'server.register(myFastifyPlugin)', + '```', + '', + ].join('\n') + ) + ) + } + + // Initialize the fastify instance + const server: Server = Object.assign(fastify(fastifyServerOptions), { + // `start` will get replaced further down in this file + start: async () => { + throw new Error('Not implemented yet') + }, + }) + + server.addHook('onRequest', (_req, _reply, done) => { + getAsyncStoreInstance().run(new Map(), done) + }) + + await server.register(redwoodFastifyFunctions, { redwood: { apiRootPath } }) + + // If we can find `api/dist/functions/graphql.js`, register the GraphQL plugin + const [graphqlFunctionPath] = await fg('dist/functions/graphql.{ts,js}', { + cwd: getPaths().api.base, + absolute: true, + }) + + if (graphqlFunctionPath) { + const { redwoodFastifyGraphQLServer } = require('./plugins/graphql') + // This comes from a babel plugin that's applied to api/dist/functions/graphql.{ts,js} in user projects + const { __rw_graphqlOptions } = require(graphqlFunctionPath) + + await server.register(redwoodFastifyGraphQLServer, { + redwood: { + apiRootPath, + graphql: __rw_graphqlOptions, + }, + }) + } + + // For baremetal and pm2. See https://github.com/redwoodjs/redwood/pull/4744 + server.addHook('onReady', (done) => { + process.send?.('ready') + done() + }) + + // Just logging. The conditional here is to appease TS. + // `server.server.address()` can return a string, an AddressInfo object, or null. + // Note that the logging here ("Listening on...") seems to be duplicated, probably by `@redwoodjs/graphql-server` + server.addHook('onListen', (done) => { + const addressInfo = server.server.address() + + if (!addressInfo || typeof addressInfo === 'string') { + done() + return + } + + console.log( + `Listening on ${c.magenta( + `http://${addressInfo.address}:${addressInfo.port}${apiRootPath}` + )}` + ) + done() + }) + + /** + * A wrapper around `fastify.listen` that handles `--port`, `REDWOOD_API_PORT` and [api].port in redwood.toml + * + * The order of precedence is: + * - `--port` + * - `REDWOOD_API_PORT` + * - [api].port in redwood.toml + */ + server.start = (options: StartOptions = {}) => { + return server.listen({ + ...options, + port, + host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : '::', + }) + } + + return server +} + +type ResolvedOptions = Required< + Omit & { + fastifyServerOptions: FastifyServerOptions + port: number + } +> + +export function resolveOptions( + options: CreateServerOptions = {}, + args?: string[] +) { + options.logger ??= DEFAULT_CREATE_SERVER_OPTIONS.logger + + let defaultPort: number | undefined + + if (process.env.REDWOOD_API_PORT === undefined) { + defaultPort = getConfig().api.port + } else { + defaultPort = parseInt(process.env.REDWOOD_API_PORT) + } + + // Set defaults. + const resolvedOptions: ResolvedOptions = { + apiRootPath: + options.apiRootPath ?? DEFAULT_CREATE_SERVER_OPTIONS.apiRootPath, + + fastifyServerOptions: options.fastifyServerOptions ?? { + requestTimeout: + DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout, + logger: options.logger ?? DEFAULT_CREATE_SERVER_OPTIONS.logger, + }, + + port: defaultPort, + } + + // Merge fastifyServerOptions. + resolvedOptions.fastifyServerOptions.requestTimeout ??= + DEFAULT_CREATE_SERVER_OPTIONS.fastifyServerOptions.requestTimeout + resolvedOptions.fastifyServerOptions.logger = options.logger + + const { values } = parseArgs({ + options: { + apiRootPath: { + type: 'string', + }, + port: { + type: 'string', + short: 'p', + }, + }, + + // When running Jest, `process.argv` is... + // + // ```js + // [ + // 'path/to/node' + // 'path/to/jest.js' + // 'file/under/test.js' + // ] + // ``` + // + // `parseArgs` strips the first two, leaving the third, which is interpreted as a positional argument. + // Which fails our options. We'd still like to be strict, but can't do it for tests. + strict: process.env.NODE_ENV === 'test' ? false : true, + ...(args && { args }), + }) + + if (values.apiRootPath && typeof values.apiRootPath !== 'string') { + throw new Error('`apiRootPath` must be a string') + } + + if (values.apiRootPath) { + resolvedOptions.apiRootPath = values.apiRootPath + } + + // Format `apiRootPath` + if (resolvedOptions.apiRootPath.charAt(0) !== '/') { + resolvedOptions.apiRootPath = `/${resolvedOptions.apiRootPath}` + } + + if ( + resolvedOptions.apiRootPath.charAt( + resolvedOptions.apiRootPath.length - 1 + ) !== '/' + ) { + resolvedOptions.apiRootPath = `${resolvedOptions.apiRootPath}/` + } + + if (values.port) { + resolvedOptions.port = +values.port + + if (isNaN(resolvedOptions.port)) { + throw new Error('`port` must be an integer') + } + } + + return resolvedOptions +} + +type DefaultCreateServerOptions = Required< + Omit & { + fastifyServerOptions: Pick + } +> + +export const DEFAULT_CREATE_SERVER_OPTIONS: DefaultCreateServerOptions = { + apiRootPath: '/', + logger: { + level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn', + }, + fastifyServerOptions: { + requestTimeout: 15_000, + }, +} + +export interface RedwoodFastifyAPIOptions { + redwood: { + apiRootPath: string + } +} + +export async function redwoodFastifyFunctions( + fastify: FastifyInstance, + opts: RedwoodFastifyAPIOptions, + done: HookHandlerDoneFunction +) { + fastify.register(fastifyUrlData) + await fastify.register(fastifyRawBody) + + fastify.addContentTypeParser( + ['application/x-www-form-urlencoded', 'multipart/form-data'], + { parseAs: 'string' }, + fastify.defaultTextParser + ) + + fastify.all(`${opts.redwood.apiRootPath}:routeName`, lambdaRequestHandler) + fastify.all(`${opts.redwood.apiRootPath}:routeName/*`, lambdaRequestHandler) + + await loadFunctionsFromDist({ + fastGlobOptions: { + ignore: ['**/dist/functions/graphql.js'], + }, + }) + + done() +} diff --git a/packages/fastify/src/graphql.ts b/packages/api-server/src/plugins/graphql.ts similarity index 66% rename from packages/fastify/src/graphql.ts rename to packages/api-server/src/plugins/graphql.ts index b3d1ef5d06b1..86bdb7980eba 100644 --- a/packages/fastify/src/graphql.ts +++ b/packages/api-server/src/plugins/graphql.ts @@ -1,4 +1,5 @@ import fastifyUrlData from '@fastify/url-data' +import fg from 'fast-glob' import type { FastifyInstance, HTTPMethods, @@ -9,16 +10,22 @@ import type { import fastifyRawBody from 'fastify-raw-body' import type { Plugin } from 'graphql-yoga' -import type { GlobalContext } from '@redwoodjs/context' -import { getAsyncStoreInstance } from '@redwoodjs/context/dist/store' -import type { GraphQLYogaOptions } from '@redwoodjs/graphql-server' import { createGraphQLYoga } from '@redwoodjs/graphql-server' +import type { GraphQLYogaOptions } from '@redwoodjs/graphql-server' +import { getPaths } from '@redwoodjs/project-config' /** * Transform a Fastify Request to an event compatible with the RedwoodGraphQLContext's event * which is based on the AWS Lambda event */ -import { lambdaEventForFastifyRequest as transformToRedwoodGraphQLContextEvent } from './lambda/index' +import { lambdaEventForFastifyRequest } from '../requestHandlers/awsLambdaFastify' + +export interface RedwoodFastifyGraphQLOptions { + redwood: { + apiRootPath: string + graphql?: GraphQLYogaOptions + } +} /** * Redwood GraphQL Server Fastify plugin based on GraphQL Yoga @@ -28,7 +35,7 @@ import { lambdaEventForFastifyRequest as transformToRedwoodGraphQLContextEvent } */ export async function redwoodFastifyGraphQLServer( fastify: FastifyInstance, - options: GraphQLYogaOptions, + options: RedwoodFastifyGraphQLOptions, done: HookHandlerDoneFunction ) { // These two plugins are needed to transform a Fastify Request to a Lambda event @@ -42,32 +49,39 @@ export async function redwoodFastifyGraphQLServer( try { const method = ['GET', 'POST', 'OPTIONS'] as HTTPMethods[] - // TODO: This should be refactored to only be defined once and it might not live here - // Ensure that each request has a unique global context - fastify.addHook('onRequest', (_req, _reply, done) => { - getAsyncStoreInstance().run(new Map(), done) - }) + // Load the graphql options from the graphql function if none are explicitly provided + if (!options.redwood.graphql) { + const [graphqlFunctionPath] = await fg('dist/functions/graphql.{ts,js}', { + cwd: getPaths().api.base, + absolute: true, + }) + + const { __rw_graphqlOptions } = await import(graphqlFunctionPath) + options.redwood.graphql = __rw_graphqlOptions as GraphQLYogaOptions + } + + const graphqlOptions = options.redwood.graphql // Here we can add any plugins that we want to use with GraphQL Yoga Server // that we do not want to add the the GraphQLHandler in the graphql-server // graphql function. // // These would be plugins that need a server instance such as Redwood Realtime - if (options.realtime) { + if (graphqlOptions.realtime) { const { useRedwoodRealtime } = await import('@redwoodjs/realtime') const originalExtraPlugins: Array> = - options.extraPlugins || [] - originalExtraPlugins.push(useRedwoodRealtime(options.realtime)) - options.extraPlugins = originalExtraPlugins + graphqlOptions.extraPlugins || [] + originalExtraPlugins.push(useRedwoodRealtime(graphqlOptions.realtime)) + graphqlOptions.extraPlugins = originalExtraPlugins // uses for SSE single connection mode with the `/graphql/stream` endpoint - if (options.realtime.subscriptions) { + if (graphqlOptions.realtime.subscriptions) { method.push('PUT') } } - const { yoga } = createGraphQLYoga(options) + const { yoga } = createGraphQLYoga(graphqlOptions) const graphQLYogaHandler = async ( req: FastifyRequest, @@ -76,7 +90,7 @@ export async function redwoodFastifyGraphQLServer( const response = await yoga.handleNodeRequest(req, { req, reply, - event: transformToRedwoodGraphQLContextEvent(req), + event: lambdaEventForFastifyRequest(req), requestContext: {}, }) @@ -91,14 +105,15 @@ export async function redwoodFastifyGraphQLServer( } const routePaths = ['', '/health', '/readiness', '/stream'] - - routePaths.forEach((routePath) => { + for (const routePath of routePaths) { fastify.route({ - url: `${yoga.graphqlEndpoint}${routePath}`, + url: `${options.redwood.apiRootPath}${formatGraphQLEndpoint( + yoga.graphqlEndpoint + )}${routePath}`, method, handler: async (req, reply) => await graphQLYogaHandler(req, reply), }) - }) + } fastify.ready(() => { console.info(`GraphQL Yoga Server endpoint at ${yoga.graphqlEndpoint}`) @@ -115,3 +130,7 @@ export async function redwoodFastifyGraphQLServer( console.log(e) } } + +function formatGraphQLEndpoint(endpoint: string) { + return endpoint.replace(/^\//, '').replace(/\/$/, '') +} diff --git a/packages/api-server/src/plugins/lambdaLoader.ts b/packages/api-server/src/plugins/lambdaLoader.ts index c345150c36b4..ebcdce6fb1bb 100644 --- a/packages/api-server/src/plugins/lambdaLoader.ts +++ b/packages/api-server/src/plugins/lambdaLoader.ts @@ -3,6 +3,7 @@ import path from 'path' import c from 'ansi-colors' import type { Handler } from 'aws-lambda' import fg from 'fast-glob' +import type { Options as FastGlobOptions } from 'fast-glob' import type { FastifyReply, FastifyRequest, @@ -54,9 +55,19 @@ export const setLambdaFunctions = async (foundFunctions: string[]) => { }) } +type LoadFunctionsFromDistOptions = { + fastGlobOptions?: FastGlobOptions +} + // TODO: Use v8 caching to load these crazy fast. -export const loadFunctionsFromDist = async () => { - const serverFunctions = findApiDistFunctions() +export const loadFunctionsFromDist = async ( + options: LoadFunctionsFromDistOptions = {} +) => { + const serverFunctions = findApiDistFunctions( + getPaths().api.base, + options?.fastGlobOptions + ) + // Place `GraphQL` serverless function at the start. const i = serverFunctions.findIndex((x) => x.indexOf('graphql') !== -1) if (i >= 0) { @@ -68,11 +79,15 @@ export const loadFunctionsFromDist = async () => { // NOTE: Copied from @redwoodjs/internal/dist/files to avoid depending on @redwoodjs/internal. // import { findApiDistFunctions } from '@redwoodjs/internal/dist/files' -function findApiDistFunctions(cwd: string = getPaths().api.base) { +function findApiDistFunctions( + cwd: string = getPaths().api.base, + options: FastGlobOptions = {} +) { return fg.sync('dist/functions/**/*.{ts,js}', { cwd, deep: 2, // We don't support deeply nested api functions, to maximise compatibility with deployment providers absolute: true, + ...options, }) } diff --git a/packages/api-server/src/requestHandlers/awsLambdaFastify.ts b/packages/api-server/src/requestHandlers/awsLambdaFastify.ts index e4b0efd32e8a..b68fc38a123c 100644 --- a/packages/api-server/src/requestHandlers/awsLambdaFastify.ts +++ b/packages/api-server/src/requestHandlers/awsLambdaFastify.ts @@ -8,7 +8,7 @@ import qs from 'qs' import { mergeMultiValueHeaders, parseBody } from './utils' -const lambdaEventForFastifyRequest = ( +export const lambdaEventForFastifyRequest = ( request: FastifyRequest ): APIGatewayProxyEvent => { return { diff --git a/packages/api-server/src/watch.ts b/packages/api-server/src/watch.ts index a4aa51a4fe12..1888a660304b 100644 --- a/packages/api-server/src/watch.ts +++ b/packages/api-server/src/watch.ts @@ -6,7 +6,6 @@ import fs from 'fs' import path from 'path' import c from 'ansi-colors' -import chalk from 'chalk' import chokidar from 'chokidar' import dotenv from 'dotenv' import { debounce } from 'lodash' @@ -32,7 +31,6 @@ const argv = yargs(hideBin(process.argv)) description: 'Debugging port', type: 'number', }) - // `port` is not used when server-file is used .option('port', { alias: 'p', description: 'Port', @@ -131,20 +129,13 @@ const buildAndRestart = async ({ // Start API server - // Check if experimental server file exists const serverFile = resolveFile(`${rwjsPaths.api.dist}/server`) if (serverFile) { - const separator = chalk.hex('#ff845e')('-'.repeat(79)) - console.log( - [ - separator, - `🧪 ${chalk.green('Experimental Feature')} 🧪`, - separator, - 'Using the experimental API server file at api/dist/server.js (in watch mode)', - separator, - ].join('\n') + httpServerProcess = fork( + serverFile, + ['--port', port.toString()], + forkOpts ) - httpServerProcess = fork(serverFile, [], forkOpts) } else { httpServerProcess = fork( path.join(__dirname, 'index.js'), diff --git a/packages/babel-config/src/api.ts b/packages/babel-config/src/api.ts index 53c05cc56801..0de4f471de19 100644 --- a/packages/babel-config/src/api.ts +++ b/packages/babel-config/src/api.ts @@ -152,6 +152,16 @@ export const getApiSideBabelConfigPath = () => { export const getApiSideBabelOverrides = () => { const overrides = [ + // Extract graphql options from the graphql function + // NOTE: this must come before the context wrapping + { + // match */api/src/functions/graphql.js|ts + test: /.+api(?:[\\|/])src(?:[\\|/])functions(?:[\\|/])graphql\.(?:js|ts)$/, + plugins: [ + require('./plugins/babel-plugin-redwood-graphql-options-extract') + .default, + ], + }, // Apply context wrapping to all functions { // match */api/src/functions/*.js|ts diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/default-graphql-function/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/default-graphql-function/code.js new file mode 100644 index 000000000000..f395c3b0f852 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/default-graphql-function/code.js @@ -0,0 +1,19 @@ +import { createGraphQLHandler } from '@redwoodjs/graphql-server' + +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' + +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' + +export const handler = createGraphQLHandler({ + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, +}) diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/default-graphql-function/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/default-graphql-function/output.js new file mode 100644 index 000000000000..0bb586e9f8b7 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/default-graphql-function/output.js @@ -0,0 +1,20 @@ +import { createGraphQLHandler } from '@redwoodjs/graphql-server' +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' +export const __rw_graphqlOptions = { + loggerConfig: { + logger, + options: {}, + }, + directives, + sdls, + services, + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, +} +export const handler = createGraphQLHandler(__rw_graphqlOptions) \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/evil-graphql-function/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/evil-graphql-function/code.js new file mode 100644 index 000000000000..b18cec542546 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/evil-graphql-function/code.js @@ -0,0 +1,36 @@ +import { createGraphQLHandler } from '@redwoodjs/graphql-server' + +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' + +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' + +export const handling = () => { + console.log("handling") +} + +const config = { + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + onException() { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, + extraPlugins: [ + { + name: 'test', + function: () => {console.log('test')} + } + ], + graphiQLEndpoint: 'coolness', + allowGraphiQL: false, +} + +/** + * Comments... + */ +export const handler = createGraphQLHandler(process.env.EVIL ? config : {sadness: true}) diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/evil-graphql-function/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/evil-graphql-function/output.js new file mode 100644 index 000000000000..d69716078af2 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/evil-graphql-function/output.js @@ -0,0 +1,42 @@ +import { createGraphQLHandler } from '@redwoodjs/graphql-server' +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' +export const handling = () => { + console.log('handling') +} +const config = { + loggerConfig: { + logger, + options: {}, + }, + directives, + sdls, + services, + onException() { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, + extraPlugins: [ + { + name: 'test', + function: () => { + console.log('test') + }, + }, + ], + graphiQLEndpoint: 'coolness', + allowGraphiQL: false, +} + +/** + * Comments... + */ +export const __rw_graphqlOptions = process.env.EVIL + ? config + : { + sadness: true, + } +export const handler = createGraphQLHandler(__rw_graphqlOptions) \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/function-graphql-function/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/function-graphql-function/code.js new file mode 100644 index 000000000000..a94bc8788cef --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/function-graphql-function/code.js @@ -0,0 +1,21 @@ +import { createGraphQLHandler } from '@redwoodjs/graphql-server' + +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' + +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' + +const config = () => ({ + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, +}) + +export const handler = createGraphQLHandler(config()) diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/function-graphql-function/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/function-graphql-function/output.js new file mode 100644 index 000000000000..b319ddf9e8b6 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/function-graphql-function/output.js @@ -0,0 +1,21 @@ +import { createGraphQLHandler } from '@redwoodjs/graphql-server' +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' +const config = () => ({ + loggerConfig: { + logger, + options: {}, + }, + directives, + sdls, + services, + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, +}) +export const __rw_graphqlOptions = config() +export const handler = createGraphQLHandler(__rw_graphqlOptions) \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/modified-graphql-function/code.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/modified-graphql-function/code.js new file mode 100644 index 000000000000..7083f6d9313a --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/modified-graphql-function/code.js @@ -0,0 +1,36 @@ +import { createGraphQLHandler } from '@redwoodjs/graphql-server' + +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' + +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' + +export const handling = () => { + console.log("handling") +} + +const config = { + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + onException() { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, + extraPlugins: [ + { + name: 'test', + function: () => {console.log('test')} + } + ], + graphiQLEndpoint: 'coolness', + allowGraphiQL: false, +} + +/** + * Comments... + */ +export const handler = createGraphQLHandler(config) diff --git a/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/modified-graphql-function/output.js b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/modified-graphql-function/output.js new file mode 100644 index 000000000000..bd29d0011d17 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/__fixtures__/graphql-options-extract/modified-graphql-function/output.js @@ -0,0 +1,38 @@ +import { createGraphQLHandler } from '@redwoodjs/graphql-server' +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' +import { db } from 'src/lib/db' +import { logger } from 'src/lib/logger' +export const handling = () => { + console.log('handling') +} +const config = { + loggerConfig: { + logger, + options: {}, + }, + directives, + sdls, + services, + onException() { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, + extraPlugins: [ + { + name: 'test', + function: () => { + console.log('test') + }, + }, + ], + graphiQLEndpoint: 'coolness', + allowGraphiQL: false, +} + +/** + * Comments... + */ +export const __rw_graphqlOptions = config +export const handler = createGraphQLHandler(__rw_graphqlOptions) \ No newline at end of file diff --git a/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-graphql-options-extract.test.ts b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-graphql-options-extract.test.ts new file mode 100644 index 000000000000..86ffae891392 --- /dev/null +++ b/packages/babel-config/src/plugins/__tests__/babel-plugin-redwood-graphql-options-extract.test.ts @@ -0,0 +1,19 @@ +import path from 'path' + +import pluginTester from 'babel-plugin-tester' + +import redwoodGraphqlOptionsExtract from '../babel-plugin-redwood-graphql-options-extract' + +jest.mock('@redwoodjs/project-config', () => { + return { + getBaseDirFromFile: () => { + return '' + }, + } +}) + +pluginTester({ + plugin: redwoodGraphqlOptionsExtract, + pluginName: 'babel-plugin-redwood-graphql-options-extract', + fixtures: path.join(__dirname, '__fixtures__/graphql-options-extract'), +}) diff --git a/packages/babel-config/src/plugins/babel-plugin-redwood-graphql-options-extract.ts b/packages/babel-config/src/plugins/babel-plugin-redwood-graphql-options-extract.ts new file mode 100644 index 000000000000..34e39c7aa276 --- /dev/null +++ b/packages/babel-config/src/plugins/babel-plugin-redwood-graphql-options-extract.ts @@ -0,0 +1,69 @@ +// import type { NodePath, PluginObj, types } from '@babel/core' +import type { PluginObj, PluginPass, types } from '@babel/core' + +// This extracts the options passed to the graphql function and stores them in a file so they can be imported elsewhere. + +const exportVariableName = '__rw_graphqlOptions' as const + +function optionsConstNode( + t: typeof types, + value: + | types.ArgumentPlaceholder + | types.JSXNamespacedName + | types.SpreadElement + | types.Expression, + state: PluginPass +) { + if ( + t.isIdentifier(value) || + t.isObjectExpression(value) || + t.isCallExpression(value) || + t.isConditionalExpression(value) + ) { + return t.exportNamedDeclaration( + t.variableDeclaration('const', [ + t.variableDeclarator(t.identifier(exportVariableName), value), + ]) + ) + } else { + throw new Error( + `Unable to parse graphql function options in '${state.file.opts.filename}'` + ) + } +} + +export default function ({ types: t }: { types: typeof types }): PluginObj { + return { + name: 'babel-plugin-redwood-graphql-options-extract', + visitor: { + ExportNamedDeclaration(path, state) { + const declaration = path.node.declaration + if (declaration?.type !== 'VariableDeclaration') { + return + } + + const declarator = declaration.declarations[0] + if (declarator?.type !== 'VariableDeclarator') { + return + } + + const identifier = declarator.id + if (identifier?.type !== 'Identifier') { + return + } + if (identifier.name !== 'handler') { + return + } + + const init = declarator.init + if (init?.type !== 'CallExpression') { + return + } + + const options = init.arguments[0] + path.insertBefore(optionsConstNode(t, options, state)) + init.arguments[0] = t.identifier(exportVariableName) + }, + }, + } +} diff --git a/packages/cli/src/commands/experimental/setupServerFileHandler.js b/packages/cli/src/commands/experimental/setupServerFileHandler.js deleted file mode 100644 index b6140dae8971..000000000000 --- a/packages/cli/src/commands/experimental/setupServerFileHandler.js +++ /dev/null @@ -1,119 +0,0 @@ -import path from 'path' - -import fs from 'fs-extra' -import { Listr } from 'listr2' - -import { addApiPackages } from '@redwoodjs/cli-helpers' -import { getConfigPath } from '@redwoodjs/project-config' -import { errorTelemetry } from '@redwoodjs/telemetry' - -import { getPaths, transformTSToJS, writeFile } from '../../lib' -import c from '../../lib/colors' -import { isTypeScriptProject } from '../../lib/project' - -import { command, description, EXPERIMENTAL_TOPIC_ID } from './setupServerFile' -import { printTaskEpilogue } from './util' - -const { version } = JSON.parse( - fs.readFileSync(path.resolve(__dirname, '../../../package.json'), 'utf-8') -) - -export const setupServerFileTasks = (force = false) => { - const redwoodPaths = getPaths() - const ts = isTypeScriptProject() - - const serverFilePath = path.join( - redwoodPaths.api.src, - `server.${isTypeScriptProject() ? 'ts' : 'js'}` - ) - - return [ - { - title: 'Adding the experimental server files...', - task: () => { - const serverFileTemplateContent = fs.readFileSync( - path.resolve(__dirname, 'templates', 'server.ts.template'), - 'utf-8' - ) - - const setupScriptContent = ts - ? serverFileTemplateContent - : transformTSToJS(serverFilePath, serverFileTemplateContent) - - return [ - writeFile(serverFilePath, setupScriptContent, { - overwriteExisting: force, - }), - ] - }, - }, - { - title: 'Adding config to redwood.toml...', - task: (_ctx, task) => { - // - const redwoodTomlPath = getConfigPath() - const configContent = fs.readFileSync(redwoodTomlPath, 'utf-8') - if (!configContent.includes('[experimental.serverFile]')) { - // Use string replace to preserve comments and formatting - writeFile( - redwoodTomlPath, - configContent.concat( - `\n[experimental.serverFile]\n\tenabled = true\n` - ), - { - overwriteExisting: true, // redwood.toml always exists - } - ) - } else { - task.skip( - `The [experimental.serverFile] config block already exists in your 'redwood.toml' file.` - ) - } - }, - }, - addApiPackages([ - 'fastify', - 'chalk@4.1.2', - `@redwoodjs/fastify@${version}`, - `@redwoodjs/project-config@${version}`, - ]), - ] -} - -export async function handler({ force, verbose }) { - const tasks = new Listr( - [ - { - title: 'Confirmation', - task: async (_ctx, task) => { - const confirmation = await task.prompt({ - type: 'Confirm', - message: 'The server file is experimental. Continue?', - }) - - if (!confirmation) { - throw new Error('User aborted') - } - }, - }, - ...setupServerFileTasks(force), - { - task: () => { - printTaskEpilogue(command, description, EXPERIMENTAL_TOPIC_ID) - }, - }, - ], - { - rendererOptions: { collapseSubtasks: false, persistentOutput: true }, - renderer: verbose ? 'verbose' : 'default', - } - ) - - try { - await tasks.run() - } catch (e) { - errorTelemetry(process.argv, e.message) - console.error(c.error(e.message)) - process.exit(e?.exitCode || 1) - } -} diff --git a/packages/cli/src/commands/experimental/templates/server.ts.template b/packages/cli/src/commands/experimental/templates/server.ts.template deleted file mode 100644 index fedba89afd12..000000000000 --- a/packages/cli/src/commands/experimental/templates/server.ts.template +++ /dev/null @@ -1,113 +0,0 @@ -import { parseArgs } from 'node:util' -import path from 'path' - -import chalk from 'chalk' -import { config } from 'dotenv-defaults' -import Fastify from 'fastify' - -import { - coerceRootPath, - redwoodFastifyWeb, - redwoodFastifyAPI, - redwoodFastifyGraphQLServer, - DEFAULT_REDWOOD_FASTIFY_CONFIG, -} from '@redwoodjs/fastify' -import { getPaths, getConfig } from '@redwoodjs/project-config' - -import directives from 'src/directives/**/*.{js,ts}' -import sdls from 'src/graphql/**/*.sdl.{js,ts}' -import services from 'src/services/**/*.{js,ts}' - -// Import if using RedwoodJS authentication -// import { authDecoder } from '@redwoodjs/' -// import { getCurrentUser } from 'src/lib/auth' - -import { logger } from 'src/lib/logger' - -// Import if using RedwoodJS Realtime via `yarn rw exp setup-realtime` -// import { realtime } from 'src/lib/realtime' - -async function serve() { - // Parse server file args - const { values: args } = parseArgs({ - options: { - ['enable-web']: { - type: 'boolean', - default: false, - }, - }, - }) - const { ['enable-web']: enableWeb } = args - - // Load .env files - const redwoodProjectPaths = getPaths() - const redwoodConfig = getConfig() - - const apiRootPath = enableWeb ? coerceRootPath(redwoodConfig.web.apiUrl) : '' - const port = enableWeb ? redwoodConfig.web.port : redwoodConfig.api.port - - const tsServer = Date.now() - - config({ - path: path.join(redwoodProjectPaths.base, '.env'), - defaults: path.join(redwoodProjectPaths.base, '.env.defaults'), - multiline: true, - }) - - console.log(chalk.italic.dim('Starting API and Web Servers...')) - - // Configure Fastify - const fastify = Fastify({ - ...DEFAULT_REDWOOD_FASTIFY_CONFIG, - }) - - if (enableWeb) { - await fastify.register(redwoodFastifyWeb) - } - - await fastify.register(redwoodFastifyAPI, { - redwood: { - apiRootPath, - }, - }) - - await fastify.register(redwoodFastifyGraphQLServer, { - // If authenticating, be sure to import and add in - // authDecoder, - // getCurrentUser, - loggerConfig: { - logger: logger, - }, - graphiQLEndpoint: enableWeb ? '/.redwood/functions/graphql' : '/graphql', - sdls, - services, - directives, - allowIntrospection: true, - allowGraphiQL: true, - // Configure if using RedwoodJS Realtime - // realtime, - }) - - // Start - fastify.listen({ port }) - - fastify.ready(() => { - console.log(chalk.italic.dim('Took ' + (Date.now() - tsServer) + ' ms')) - const on = chalk.magenta(`http://localhost:${port}${apiRootPath}`) - if (enableWeb) { - const webServer = chalk.green(`http://localhost:${port}`) - console.log(`Web server started on ${webServer}`) - } - const apiServer = chalk.magenta(`http://localhost:${port}`) - console.log(`API serving from ${apiServer}`) - console.log(`API listening on ${on}`) - const graphqlEnd = chalk.magenta(`${apiRootPath}graphql`) - console.log(`GraphQL function endpoint at ${graphqlEnd}`) - }) - - process.on('exit', () => { - fastify.close() - }) -} - -serve() diff --git a/packages/cli/src/commands/serve.js b/packages/cli/src/commands/serve.js index 296ac6715871..af2e961b211f 100644 --- a/packages/cli/src/commands/serve.js +++ b/packages/cli/src/commands/serve.js @@ -13,7 +13,7 @@ import { webServerHandler, webSsrServerHandler } from './serveWebHandler' export const command = 'serve [side]' export const description = 'Run server for api or web in production' -function hasExperimentalServerFile() { +function hasServerFile() { const serverFilePath = path.join(getPaths().api.dist, 'server.js') return fs.existsSync(serverFilePath) } @@ -24,15 +24,34 @@ export const builder = async (yargs) => { .command({ command: '$0', description: 'Run both api and web servers', - builder: (yargs) => - yargs.options({ - port: { - default: getConfig().web?.port || 8910, - type: 'number', - alias: 'p', - }, - socket: { type: 'string' }, - }), + builder: (yargs) => { + if (!hasServerFile()) { + yargs.options({ + port: { + default: getConfig().web?.port || 8910, + type: 'number', + alias: 'p', + }, + socket: { type: 'string' }, + }) + + return + } + + yargs + .options({ + webPort: { + default: getConfig().web?.port || 8910, + type: 'number', + }, + }) + .options({ + apiPort: { + default: getConfig().api?.port || 8911, + type: 'number', + }, + }) + }, handler: async (argv) => { recordTelemetryAttributes({ command: 'serve', @@ -41,12 +60,12 @@ export const builder = async (yargs) => { socket: argv.socket, }) - // Run the experimental server file, if it exists, with web side also - if (hasExperimentalServerFile()) { - const { bothExperimentalServerFileHandler } = await import( + // Run the server file, if it exists, with web side also + if (hasServerFile()) { + const { bothServerFileHandler } = await import( './serveBothHandler.js' ) - await bothExperimentalServerFileHandler() + await bothServerFileHandler(argv) } else if ( getConfig().experimental?.rsc?.enabled || getConfig().experimental?.streamingSsr?.enabled @@ -96,12 +115,10 @@ export const builder = async (yargs) => { apiRootPath: argv.apiRootPath, }) - // Run the experimental server file, if it exists, api side only - if (hasExperimentalServerFile()) { - const { apiExperimentalServerFileHandler } = await import( - './serveApiHandler.js' - ) - await apiExperimentalServerFileHandler() + // Run the server file, if it exists, api side only + if (hasServerFile()) { + const { apiServerFileHandler } = await import('./serveApiHandler.js') + await apiServerFileHandler(argv) } else { const { apiServerHandler } = await import('./serveApiHandler.js') await apiServerHandler(argv) diff --git a/packages/cli/src/commands/serveApiHandler.js b/packages/cli/src/commands/serveApiHandler.js index 278d5031b51a..1b1195ab0ee4 100644 --- a/packages/cli/src/commands/serveApiHandler.js +++ b/packages/cli/src/commands/serveApiHandler.js @@ -6,15 +6,22 @@ import execa from 'execa' import { createFastifyInstance, redwoodFastifyAPI } from '@redwoodjs/fastify' import { getPaths } from '@redwoodjs/project-config' -export const apiExperimentalServerFileHandler = async () => { - logExperimentalHeader() - - await execa('yarn', ['node', path.join('dist', 'server.js')], { - cwd: getPaths().api.base, - stdio: 'inherit', - shell: true, - }) - return +export const apiServerFileHandler = async (argv) => { + await execa( + 'yarn', + [ + 'node', + path.join('dist', 'server.js'), + '--port', + argv.port, + '--apiRootPath', + argv.apiRootPath, + ], + { + cwd: getPaths().api.base, + stdio: 'inherit', + } + ) } export const apiServerHandler = async (options) => { @@ -71,19 +78,3 @@ export const apiServerHandler = async (options) => { function sendProcessReady() { return process.send && process.send('ready') } - -const separator = chalk.hex('#ff845e')( - '------------------------------------------------------------------' -) - -function logExperimentalHeader() { - console.log( - [ - separator, - `🧪 ${chalk.green('Experimental Feature')} 🧪`, - separator, - 'Using the experimental API server file at api/dist/server.js', - separator, - ].join('\n') - ) -} diff --git a/packages/cli/src/commands/serveBothHandler.js b/packages/cli/src/commands/serveBothHandler.js index c7892917116a..3dd24ac7c650 100644 --- a/packages/cli/src/commands/serveBothHandler.js +++ b/packages/cli/src/commands/serveBothHandler.js @@ -1,6 +1,7 @@ import path from 'path' import chalk from 'chalk' +import concurrently from 'concurrently' import execa from 'execa' import { @@ -10,10 +11,11 @@ import { redwoodFastifyWeb, } from '@redwoodjs/fastify' import { getConfig, getPaths } from '@redwoodjs/project-config' +import { errorTelemetry } from '@redwoodjs/telemetry' -export const bothExperimentalServerFileHandler = async () => { - logExperimentalHeader() +import { exitWithError } from '../lib/exit' +export const bothServerFileHandler = async (argv) => { if ( getConfig().experimental?.rsc?.enabled || getConfig().experimental?.streamingSsr?.enabled @@ -26,15 +28,43 @@ export const bothExperimentalServerFileHandler = async () => { shell: true, }) } else { - await execa( - 'yarn', - ['node', path.join('dist', 'server.js'), '--enable-web'], + const apiHost = `http://0.0.0.0:${argv.apiPort}` + + const { result } = concurrently( + [ + { + name: 'api', + command: `yarn node ${path.join('dist', 'server.js')} --port ${ + argv.apiPort + }`, + cwd: getPaths().api.base, + prefixColor: 'cyan', + }, + { + name: 'web', + command: `yarn rw-web-server --port ${argv.webPort} --api-host ${apiHost}`, + cwd: getPaths().base, + prefixColor: 'blue', + }, + ], { - cwd: getPaths().api.base, - stdio: 'inherit', - shell: true, + prefix: '{name} |', + timestampFormat: 'HH:mm:ss', + handleInput: true, } ) + + try { + await result + } catch (error) { + if (typeof error?.message !== 'undefined') { + errorTelemetry( + process.argv, + `Error concurrently starting sides: ${error.message}` + ) + exitWithError(error) + } + } } } @@ -122,22 +152,6 @@ function sendProcessReady() { return process.send && process.send('ready') } -const separator = chalk.hex('#ff845e')( - '------------------------------------------------------------------' -) - -function logExperimentalHeader() { - console.log( - [ - separator, - `🧪 ${chalk.green('Experimental Feature')} 🧪`, - separator, - 'Using the experimental API server file at api/dist/server.js', - separator, - ].join('\n') - ) -} - function logSkippingFastifyWebServer() { console.warn('') console.warn('⚠️ Skipping Fastify web server ⚠️') diff --git a/packages/cli/src/commands/setup/realtime/realtimeHandler.js b/packages/cli/src/commands/setup/realtime/realtimeHandler.js index 1c2d1f58107e..006258bdca7d 100644 --- a/packages/cli/src/commands/setup/realtime/realtimeHandler.js +++ b/packages/cli/src/commands/setup/realtime/realtimeHandler.js @@ -11,8 +11,8 @@ import { getPaths, transformTSToJS, writeFile } from '../../../lib' import c from '../../../lib/colors' import { isTypeScriptProject } from '../../../lib/project' // Move this check out of experimental when server file is moved as well -import { setupServerFileTasks } from '../../experimental/setupServerFileHandler' import { serverFileExists } from '../../experimental/util' +import { setupServerFileTasks } from '../server-file/serverFileHandler' const { version } = JSON.parse( fs.readFileSync(path.resolve(__dirname, '../../../../package.json'), 'utf-8') @@ -363,7 +363,7 @@ export async function handler({ force, includeExamples, verbose }) { try { if (!serverFileExists()) { - tasks.add(setupServerFileTasks(force)) + tasks.add(setupServerFileTasks({ force })) } await tasks.run() diff --git a/packages/cli/src/commands/experimental/setupServerFile.js b/packages/cli/src/commands/setup/server-file/serverFile.js similarity index 59% rename from packages/cli/src/commands/experimental/setupServerFile.js rename to packages/cli/src/commands/setup/server-file/serverFile.js index b842d3797263..46a126d7f062 100644 --- a/packages/cli/src/commands/experimental/setupServerFile.js +++ b/packages/cli/src/commands/setup/server-file/serverFile.js @@ -1,12 +1,8 @@ import { recordTelemetryAttributes } from '@redwoodjs/cli-helpers' -import { getEpilogue } from './util' +export const command = 'server-file' -export const EXPERIMENTAL_TOPIC_ID = 4851 - -export const command = 'setup-server-file' - -export const description = 'Setup the experimental server file' +export const description = 'Setup the server file' export function builder(yargs) { yargs @@ -22,15 +18,14 @@ export function builder(yargs) { description: 'Print more logs', type: 'boolean', }) - .epilogue(getEpilogue(command, description, EXPERIMENTAL_TOPIC_ID, true)) } export async function handler(options) { recordTelemetryAttributes({ - command: 'experimental setup-server-file', + command: 'setup server-file', force: options.force, verbose: options.verbose, }) - const { handler } = await import('./setupServerFileHandler.js') + const { handler } = await import('./serverFileHandler.js') return handler(options) } diff --git a/packages/cli/src/commands/setup/server-file/serverFileHandler.js b/packages/cli/src/commands/setup/server-file/serverFileHandler.js new file mode 100644 index 000000000000..6fb68e544ca6 --- /dev/null +++ b/packages/cli/src/commands/setup/server-file/serverFileHandler.js @@ -0,0 +1,62 @@ +import path from 'path' + +import fs from 'fs-extra' +import { Listr } from 'listr2' + +import { addApiPackages } from '@redwoodjs/cli-helpers' +import { errorTelemetry } from '@redwoodjs/telemetry' + +import { getPaths, transformTSToJS, writeFile } from '../../../lib' +import c from '../../../lib/colors' +import { isTypeScriptProject } from '../../../lib/project' + +const { version } = JSON.parse( + fs.readFileSync(path.resolve(__dirname, '../../../../package.json'), 'utf-8') +) + +export function setupServerFileTasks({ force = false } = {}) { + return [ + { + title: 'Adding the server file...', + task: () => { + const ts = isTypeScriptProject() + + const serverFilePath = path.join( + getPaths().api.src, + `server.${ts ? 'ts' : 'js'}` + ) + + const serverFileTemplateContent = fs.readFileSync( + path.join(__dirname, 'templates', 'server.ts.template'), + 'utf-8' + ) + + const setupScriptContent = ts + ? serverFileTemplateContent + : transformTSToJS(serverFilePath, serverFileTemplateContent) + + return [ + writeFile(serverFilePath, setupScriptContent, { + overwriteExisting: force, + }), + ] + }, + }, + addApiPackages([`@redwoodjs/api-server@${version}`]), + ] +} + +export async function handler({ force, verbose }) { + const tasks = new Listr(setupServerFileTasks({ force }), { + rendererOptions: { collapseSubtasks: false, persistentOutput: true }, + renderer: verbose ? 'verbose' : 'default', + }) + + try { + await tasks.run() + } catch (e) { + errorTelemetry(process.argv, e.message) + console.error(c.error(e.message)) + process.exit(e?.exitCode || 1) + } +} diff --git a/packages/cli/src/commands/setup/server-file/templates/server.ts.template b/packages/cli/src/commands/setup/server-file/templates/server.ts.template new file mode 100644 index 000000000000..429b9430a983 --- /dev/null +++ b/packages/cli/src/commands/setup/server-file/templates/server.ts.template @@ -0,0 +1,13 @@ +import { createServer } from '@redwoodjs/api-server' + +import { logger } from 'src/lib/logger' + +async function main() { + const server = await createServer({ + logger, + }) + + await server.start() +} + +main() diff --git a/packages/fastify/build.mjs b/packages/fastify/build.mjs index a4650dd27782..74a3ecf2f5e0 100644 --- a/packages/fastify/build.mjs +++ b/packages/fastify/build.mjs @@ -4,7 +4,6 @@ await esbuild.build({ entryPoints: [ 'src/api.ts', 'src/config.ts', - 'src/graphql.ts', 'src/index.ts', 'src/types.ts', 'src/web.ts', diff --git a/packages/fastify/package.json b/packages/fastify/package.json index 302c70c73ef2..0eb6d1619598 100644 --- a/packages/fastify/package.json +++ b/packages/fastify/package.json @@ -23,7 +23,6 @@ "@fastify/static": "6.12.0", "@fastify/url-data": "5.4.0", "@redwoodjs/context": "6.0.7", - "@redwoodjs/graphql-server": "6.0.7", "@redwoodjs/project-config": "6.0.7", "ansi-colors": "4.1.3", "fast-glob": "3.3.2", diff --git a/packages/fastify/src/index.ts b/packages/fastify/src/index.ts index e95488bea13c..e381ec32ed1c 100644 --- a/packages/fastify/src/index.ts +++ b/packages/fastify/src/index.ts @@ -11,7 +11,6 @@ export function createFastifyInstance(options?: FastifyServerOptions) { export { redwoodFastifyAPI } from './api.js' export { redwoodFastifyWeb } from './web.js' -export { redwoodFastifyGraphQLServer } from './graphql.js' export type * from './types.js' diff --git a/packages/graphql-server/src/types.ts b/packages/graphql-server/src/types.ts index 4876798e6c50..06fb3b755265 100644 --- a/packages/graphql-server/src/types.ts +++ b/packages/graphql-server/src/types.ts @@ -256,7 +256,7 @@ export type GraphQLYogaOptions = { * * Note: RedwoodRealtime is not supported */ -export type GraphQLHandlerOptions = Omit +export type GraphQLHandlerOptions = GraphQLYogaOptions export type GraphiQLOptions = Pick< GraphQLYogaOptions, diff --git a/packages/web-server/src/server.ts b/packages/web-server/src/server.ts index 974376a83c20..e6d7a8861fd2 100644 --- a/packages/web-server/src/server.ts +++ b/packages/web-server/src/server.ts @@ -123,7 +123,9 @@ async function serve() { if (options.socket) { console.log(`Web server started on ${options.socket}`) } else { - console.log(`Web server started on http://localhost:${options.port}`) + console.log( + `Web server started on http://${listenOptions.host}:${options.port}` + ) } }) diff --git a/yarn.lock b/yarn.lock index 715a8c3665c1..e3b6932ba660 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7525,12 +7525,18 @@ __metadata: fastify-raw-body: "npm:4.3.0" jest: "npm:29.7.0" lodash: "npm:4.17.21" + pino-abstract-transport: "npm:1.1.0" pretty-bytes: "npm:5.6.0" pretty-ms: "npm:7.0.1" qs: "npm:6.11.2" split2: "npm:4.2.0" typescript: "npm:5.3.3" yargs: "npm:17.7.2" + peerDependencies: + "@redwoodjs/graphql-server": 6.0.7 + peerDependenciesMeta: + "@redwoodjs/graphql-server": + optional: true bin: rw-api-server-watch: ./dist/watch.js rw-log-formatter: ./dist/logFormatter/bin.js @@ -8378,7 +8384,6 @@ __metadata: "@fastify/static": "npm:6.12.0" "@fastify/url-data": "npm:5.4.0" "@redwoodjs/context": "npm:6.0.7" - "@redwoodjs/graphql-server": "npm:6.0.7" "@redwoodjs/project-config": "npm:6.0.7" "@types/aws-lambda": "npm:8.10.126" "@types/lodash": "npm:4.14.201" @@ -27492,7 +27497,7 @@ __metadata: languageName: node linkType: hard -"pino-abstract-transport@npm:v1.1.0": +"pino-abstract-transport@npm:1.1.0, pino-abstract-transport@npm:v1.1.0": version: 1.1.0 resolution: "pino-abstract-transport@npm:1.1.0" dependencies: