diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a6d830aa149..973cc187823f 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 @@ -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 @@ -848,7 +852,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/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-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 120759bd60bc..db82efffeb01 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", ], @@ -71,10 +72,11 @@ describe('dist', () => { "type": "string", }, }, + "createServer": [Function], "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/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 ad04725e46ee..b6850227592c 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 @@ -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({ @@ -128,9 +127,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 +186,16 @@ 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 + } +} + +// 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/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/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/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 8722aa74ef40..8833d9689506 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/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 d06a3e871a00..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) @@ -122,7 +139,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/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/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/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/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') 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/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/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..e6d7a8861fd2 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,9 @@ 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://${listenOptions.host}:${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..e3b6932ba660 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" @@ -7364,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 @@ -7393,7 +7560,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 +7569,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 @@ -8217,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" @@ -8721,14 +8887,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 @@ -8784,6 +8949,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" @@ -11630,6 +11886,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" @@ -12322,7 +12632,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 @@ -12336,7 +12646,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: @@ -12363,6 +12680,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" @@ -13059,6 +13385,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" @@ -14133,6 +14466,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" @@ -14406,6 +14746,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" @@ -14538,6 +14893,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" @@ -15633,6 +15997,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 @@ -16236,6 +16601,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" @@ -17522,6 +17896,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" @@ -17927,6 +18381,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" @@ -18060,6 +18523,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" @@ -19273,6 +19753,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" @@ -19369,6 +19856,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" @@ -20536,6 +21030,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" @@ -21447,6 +21948,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" @@ -22680,7 +23188,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 @@ -23311,6 +23819,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" @@ -23621,6 +24139,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" @@ -24233,6 +24760,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" @@ -24938,6 +25472,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" @@ -25096,12 +25642,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 @@ -25706,6 +26252,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" @@ -26076,6 +26631,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" @@ -26248,6 +26812,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" @@ -26721,6 +27294,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" @@ -26784,10 +27364,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 @@ -26910,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: @@ -26991,6 +27578,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" @@ -27423,14 +28021,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 @@ -29165,6 +29763,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:." @@ -29764,6 +30416,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" @@ -29771,10 +30430,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 @@ -30305,6 +30964,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" @@ -30353,6 +31019,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" @@ -30670,6 +31343,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" @@ -30702,6 +31382,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" @@ -31236,6 +31925,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" @@ -31714,7 +32424,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 @@ -31919,6 +32629,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" @@ -32561,6 +33278,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" @@ -32601,6 +33333,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" @@ -33274,6 +34097,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"