From e94dbf5ff52bc33b364e183bba47985f683c915f Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 24 Jan 2024 13:36:04 +0000 Subject: [PATCH] chore(esm): convert `@redwoodjs/cli-helpers` to ESM (#9872) This PR converts `@redwoodjs/cli-helpers` to a dual ESM/CJS package like https://github.com/redwoodjs/redwood/pull/9870 did for `@redwoodjs/project-config`. I didn't do anything differently so see that PR for details. As soon as I converted it, the Jest tests stopped working so @Josh-Walker-GM and I bundled both changes into one PR here. With this PR, we should be able to get all the tests in https://github.com/redwoodjs/redwood/pull/9863 passing (about ~15 (out of 1000+) are skipped right now) because Vitest will be able to mock `fs` since it's imported instead of required. That'll be really nice since converting the CLI to ESM will then be possible. I'll confirm that after making this PR. Even if they don't this'll still have been worthwhile. --------- Co-authored-by: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> --- packages/cli-helpers/.babelrc.js | 1 - packages/cli-helpers/__mocks__/fs.js | 222 +----------------- packages/cli-helpers/build.js | 26 ++ packages/cli-helpers/jest.config.js | 4 - packages/cli-helpers/package.json | 21 +- .../__snapshots__/authTasks.test.ts.snap | 38 +-- .../src/auth/__tests__/authFiles.test.ts | 18 +- .../src/auth/__tests__/authTasks.test.ts | 189 +++++++-------- .../src/auth/__tests__/setupHelpers.test.ts | 29 ++- packages/cli-helpers/src/auth/authFiles.ts | 6 +- packages/cli-helpers/src/auth/authTasks.ts | 12 +- packages/cli-helpers/src/auth/setupHelpers.ts | 12 +- packages/cli-helpers/src/index.ts | 16 +- .../__snapshots__/index.test.ts.snap | 2 +- .../__snapshots__/project.test.ts.snap | 24 +- .../src/lib/__tests__/index.test.ts | 4 +- .../__tests__/project.addTomlSetting.test.ts | 15 +- .../src/lib/__tests__/project.test.ts | 37 +-- .../src/lib/__tests__/version.test.ts | 40 ++-- packages/cli-helpers/src/lib/index.ts | 4 +- .../cli-helpers/src/lib/installHelpers.ts | 2 +- packages/cli-helpers/src/lib/paths.ts | 2 +- packages/cli-helpers/src/lib/project.ts | 4 +- packages/cli-helpers/tsconfig.json | 3 +- packages/cli-helpers/vitest.config.mts | 7 + packages/project-config/tsconfig.json | 2 +- yarn.lock | 5 +- 27 files changed, 294 insertions(+), 451 deletions(-) delete mode 100644 packages/cli-helpers/.babelrc.js create mode 100644 packages/cli-helpers/build.js delete mode 100644 packages/cli-helpers/jest.config.js create mode 100644 packages/cli-helpers/vitest.config.mts diff --git a/packages/cli-helpers/.babelrc.js b/packages/cli-helpers/.babelrc.js deleted file mode 100644 index 3b2c815712d9..000000000000 --- a/packages/cli-helpers/.babelrc.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = { extends: '../../babel.config.js' } diff --git a/packages/cli-helpers/__mocks__/fs.js b/packages/cli-helpers/__mocks__/fs.js index 99fec09d82ed..de739ddd902a 100644 --- a/packages/cli-helpers/__mocks__/fs.js +++ b/packages/cli-helpers/__mocks__/fs.js @@ -1,220 +1,4 @@ -import path from 'path' +import * as memfs from 'memfs' -const fs = { - ...jest.requireActual('fs'), -} - -let mockFiles = {} - -const pathSeparator = path.sep - -const getParentDir = (path) => { - return path.substring(0, path.lastIndexOf(pathSeparator)) -} - -const makeParentDirs = (path) => { - const parentDir = getParentDir(path) - if (parentDir && !(parentDir in mockFiles)) { - mockFiles[parentDir] = undefined - makeParentDirs(parentDir) - } -} - -/** - * This is a custom function that our tests can use during setup to specify - * what the files on the "mock" filesystem should look like when any of the - * `fs` APIs are used. - * - * Sets the state of the mocked file system - * @param newMockFiles - {[filepath]: contents} - */ -fs.__setMockFiles = (newMockFiles) => { - mockFiles = { ...newMockFiles } - - // Generate all the directories which implicitly exist - Object.keys(mockFiles).forEach((mockPath) => { - if (mockPath.includes(pathSeparator)) { - makeParentDirs(mockPath) - } - }) -} - -fs.__getMockFiles = () => { - return mockFiles -} - -fs.readFileSync = (path) => { - // In prisma v4.3.0, prisma format uses a Wasm module. See https://github.com/prisma/prisma/releases/tag/4.3.0. - // We shouldn't mock this, so we'll use the real fs.readFileSync. - if (path.includes('prisma_fmt_build_bg.wasm')) { - return jest.requireActual('fs').readFileSync(path) - } - - if (path in mockFiles) { - return mockFiles[path] - } else { - const fakeError = new Error( - `Error: ENOENT: no such file or directory, open '${path}'` - ) - fakeError.errno = -2 - fakeError.syscall = 'open' - fakeError.code = 'ENOENT' - fakeError.path = path - throw fakeError - } -} - -fs.writeFileSync = (path, contents) => { - const parentDir = getParentDir(path) - if (parentDir && !fs.existsSync(parentDir)) { - const fakeError = new Error( - `Error: ENOENT: no such file or directory, open '${path}'` - ) - fakeError.errno = -2 - fakeError.syscall = 'open' - fakeError.code = 'ENOENT' - fakeError.path = path - throw fakeError - } - mockFiles[path] = contents -} - -fs.appendFileSync = (path, contents) => { - if (path in mockFiles) { - mockFiles[path] = mockFiles[path] + contents - } else { - fs.writeFileSync(path, contents) - } -} - -fs.rmSync = (path, options = {}) => { - if (fs.existsSync(path)) { - if (options.recursive) { - Object.keys(mockFiles).forEach((mockedPath) => { - if (mockedPath.startsWith(path)) { - delete mockFiles[mockedPath] - } - }) - } else { - if (mockFiles[path] === undefined) { - const children = fs.readdirSync(path) - if (children.length !== 0) { - const fakeError = new Error( - `NodeError [SystemError]: Path is a directory: rm returned EISDIR (is a directory) ${path}` - ) - fakeError.errno = 21 - fakeError.syscall = 'rm' - fakeError.code = 'ERR_FS_EISDIR' - fakeError.path = path - throw fakeError - } - } - delete mockFiles[path] - } - } else { - const fakeError = new Error( - `Error: ENOENT: no such file or directory, stat '${path}'` - ) - fakeError.errno = -2 - fakeError.syscall = 'stat' - fakeError.code = 'ENOENT' - fakeError.path = path - throw fakeError - } -} - -fs.unlinkSync = (path) => { - if (path in mockFiles) { - delete mockFiles[path] - } else { - const fakeError = new Error( - `Error: ENOENT: no such file or directory, stat '${path}'` - ) - fakeError.errno = -2 - fakeError.syscall = 'unlink' - fakeError.code = 'ENOENT' - fakeError.path = path - throw fakeError - } -} - -fs.existsSync = (path) => { - return path in mockFiles -} - -fs.copyFileSync = (src, dist) => { - fs.writeFileSync(dist, fs.readFileSync(src)) -} - -fs.readdirSync = (path) => { - if (!fs.existsSync(path)) { - const fakeError = new Error( - `Error: ENOENT: no such file or directory, scandir '${path}'` - ) - fakeError.errno = -2 - fakeError.syscall = 'scandir' - fakeError.code = 'ENOENT' - fakeError.path = path - throw fakeError - } - - if (mockFiles[path] !== undefined) { - const fakeError = new Error( - `Error: ENOTDIR: not a directory, scandir '${path}'` - ) - fakeError.errno = -20 - fakeError.syscall = 'scandir' - fakeError.code = 'ENOTDIR' - fakeError.path = path - throw fakeError - } - - const content = [] - Object.keys(mockFiles).forEach((mockedPath) => { - const childPath = mockedPath.substring(path.length + 1) - if ( - mockedPath.startsWith(path) && - !childPath.includes(pathSeparator) && - childPath - ) { - content.push(childPath) - } - }) - return content -} - -fs.mkdirSync = (path, options = {}) => { - if (options.recursive) { - makeParentDirs(path) - } - // Directories are represented as paths with an "undefined" value - fs.writeFileSync(path, undefined) -} - -fs.rmdirSync = (path, options = {}) => { - if (!fs.existsSync(path)) { - const fakeError = new Error( - `Error: ENOENT: no such file or directory, rmdir '${path}'` - ) - fakeError.errno = -2 - fakeError.syscall = 'rmdir' - fakeError.code = 'ENOENT' - fakeError.path = path - throw fakeError - } - - if (mockFiles[path] !== undefined) { - const fakeError = new Error( - `Error: ENOTDIR: not a directory, rmdir '${path}'` - ) - fakeError.errno = -20 - fakeError.syscall = 'rmdir' - fakeError.code = 'ENOTDIR' - fakeError.path = path - throw fakeError - } - - fs.rmSync(path, options) -} - -module.exports = fs +export * from 'memfs' +export default memfs.fs diff --git a/packages/cli-helpers/build.js b/packages/cli-helpers/build.js new file mode 100644 index 000000000000..3a01088401ee --- /dev/null +++ b/packages/cli-helpers/build.js @@ -0,0 +1,26 @@ +import * as esbuild from 'esbuild' + +const options = { + entryPoints: ['./src/index.ts'], + outdir: 'dist', + + platform: 'node', + target: ['node20'], + bundle: true, + packages: 'external', + + logLevel: 'info', + metafile: true, +} + +await esbuild.build({ + ...options, + format: 'esm', + outExtension: { '.js': '.mjs' }, +}) + +await esbuild.build({ + ...options, + format: 'cjs', + outExtension: { '.js': '.cjs' }, +}) diff --git a/packages/cli-helpers/jest.config.js b/packages/cli-helpers/jest.config.js deleted file mode 100644 index 4b24969ced25..000000000000 --- a/packages/cli-helpers/jest.config.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { - testPathIgnorePatterns: ['fixtures', 'dist', 'mockFsFiles'], -} diff --git a/packages/cli-helpers/package.json b/packages/cli-helpers/package.json index 8bdc55d1ef3e..d7678c25d735 100644 --- a/packages/cli-helpers/package.json +++ b/packages/cli-helpers/package.json @@ -7,30 +7,32 @@ "directory": "packages/cli-helpers" }, "license": "MIT", - "main": "./dist/index.js", + "type": "module", + "exports": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "default": "./dist/index.cjs" + }, "types": "./dist/index.d.ts", "files": [ "dist" ], "scripts": { - "build": "yarn build:js && yarn build:types", - "build:js": "babel src -d dist --extensions \".js,.jsx,.ts,.tsx\"", + "build": "yarn node ./build.js && yarn build:types", "build:pack": "yarn pack -o redwoodjs-cli-helpers.tgz", "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", + "test:watch": "vitest watch" }, "dependencies": { "@babel/core": "^7.22.20", - "@babel/runtime-corejs3": "7.23.6", "@iarna/toml": "2.2.5", "@opentelemetry/api": "1.7.0", "@redwoodjs/project-config": "6.0.7", "@redwoodjs/telemetry": "6.0.7", "chalk": "4.1.2", - "core-js": "3.34.0", "dotenv": "16.3.1", "execa": "5.1.1", "listr2": "6.6.1", @@ -41,12 +43,11 @@ "terminal-link": "2.1.1" }, "devDependencies": { - "@babel/cli": "7.23.4", "@types/lodash": "4.14.201", "@types/pascalcase": "1.0.3", "@types/yargs": "17.0.32", - "jest": "29.7.0", - "typescript": "5.3.3" + "typescript": "5.3.3", + "vitest": "1.2.1" }, "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1" } diff --git a/packages/cli-helpers/src/auth/__tests__/__snapshots__/authTasks.test.ts.snap b/packages/cli-helpers/src/auth/__tests__/__snapshots__/authTasks.test.ts.snap index 585825351020..abc555315d2b 100644 --- a/packages/cli-helpers/src/auth/__tests__/__snapshots__/authTasks.test.ts.snap +++ b/packages/cli-helpers/src/auth/__tests__/__snapshots__/authTasks.test.ts.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`authTasks Components with props Should add useAuth on the same line for single line components, and separate line for multiline components 1`] = ` +exports[`authTasks > Components with props > Should add useAuth on the same line for single line components, and separate line for multiline components 1`] = ` "import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web' import { RedwoodApolloProvider } from '@redwoodjs/web/apollo' @@ -27,7 +27,7 @@ export default App " `; -exports[`authTasks Components with props Should add useAuth on the same line for single line components, and separate line for multiline components 2`] = ` +exports[`authTasks > Components with props > Should add useAuth on the same line for single line components, and separate line for multiline components 2`] = ` "// In this file, all Page components from 'src/pages\` are auto-imported. Nested // directories are supported, and should be uppercase. Each subdirectory will be // prepended onto the component name. @@ -62,7 +62,7 @@ export default Routes " `; -exports[`authTasks Components with props Should not add useAuth if one already exists 1`] = ` +exports[`authTasks > Components with props > Should not add useAuth if one already exists 1`] = ` "import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web' import { RedwoodApolloProvider } from '@redwoodjs/web/apollo' @@ -89,7 +89,7 @@ export default App " `; -exports[`authTasks Components with props Should not add useAuth if one already exists 2`] = ` +exports[`authTasks > Components with props > Should not add useAuth if one already exists 2`] = ` "// In this file, all Page components from 'src/pages\` are auto-imported. Nested // directories are supported, and should be uppercase. Each subdirectory will be // prepended onto the component name. @@ -128,7 +128,7 @@ export default Routes " `; -exports[`authTasks Customized App.js Should add auth config when using explicit return 1`] = ` +exports[`authTasks > Customized App.js > Should add auth config when using explicit return 1`] = ` "import { useEffect } from 'react' import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web' import { RedwoodApolloProvider } from '@redwoodjs/web/apollo' @@ -166,7 +166,7 @@ export default App " `; -exports[`authTasks Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Auth0) 1`] = ` +exports[`authTasks > Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Auth0) 1`] = ` "import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web' import { RedwoodApolloProvider } from '@redwoodjs/web/apollo' @@ -193,7 +193,7 @@ export default App " `; -exports[`authTasks Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Auth0) 2`] = ` +exports[`authTasks > Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Auth0) 2`] = ` "import { Auth0Client } from '@auth0/auth0-spa-js' import { createAuth } from '@redwoodjs/auth-auth0-web' @@ -221,7 +221,7 @@ export const { AuthProvider, useAuth } = createAuth(auth0) " `; -exports[`authTasks Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Auth0) 3`] = ` +exports[`authTasks > Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Auth0) 3`] = ` "// In this file, all Page components from 'src/pages\` are auto-imported. Nested // directories are supported, and should be uppercase. Each subdirectory will be // prepended onto the component name. @@ -247,7 +247,7 @@ export default Routes " `; -exports[`authTasks Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Clerk) 1`] = ` +exports[`authTasks > Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Clerk) 1`] = ` "import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web' import { RedwoodApolloProvider } from '@redwoodjs/web/apollo' @@ -274,7 +274,7 @@ export default App " `; -exports[`authTasks Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Clerk) 2`] = ` +exports[`authTasks > Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Clerk) 2`] = ` "import React, { useEffect } from 'react' import { ClerkLoaded, ClerkProvider, useUser } from '@clerk/clerk-react' @@ -327,7 +327,7 @@ export const AuthProvider = ({ children }: Props) => { " `; -exports[`authTasks Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Clerk) 3`] = ` +exports[`authTasks > Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Clerk) 3`] = ` "// In this file, all Page components from 'src/pages\` are auto-imported. Nested // directories are supported, and should be uppercase. Each subdirectory will be // prepended onto the component name. @@ -353,7 +353,7 @@ export default Routes " `; -exports[`authTasks Should update App.tsx for legacy apps 1`] = ` +exports[`authTasks > Should update App.tsx for legacy apps 1`] = ` "import netlifyIdentity from 'netlify-identity-widget' import { isBrowser } from '@redwoodjs/prerender/browserUtils' @@ -385,7 +385,7 @@ export default App " `; -exports[`authTasks Swapped out GraphQL client Should add auth config when app is missing RedwoodApolloProvider 1`] = ` +exports[`authTasks > Swapped out GraphQL client > Should add auth config when app is missing RedwoodApolloProvider 1`] = ` "import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web' import FatalErrorPage from 'src/pages/FatalErrorPage' @@ -415,7 +415,7 @@ export default App " `; -exports[`authTasks addApiConfig Adds authDecoder arg to default graphql.ts file 1`] = ` +exports[`authTasks > addApiConfig > Adds authDecoder arg to default graphql.ts file 1`] = ` "import { authDecoder } from 'test-auth-api' import { createGraphQLHandler } from '@redwoodjs/graphql-server' @@ -442,7 +442,7 @@ export const handler = createGraphQLHandler({ " `; -exports[`authTasks addApiConfig Doesn't add authDecoder arg if one already exists 1`] = ` +exports[`authTasks > addApiConfig > Doesn't add authDecoder arg if one already exists 1`] = ` "import { authDecoder } from 'test-auth-api' import { createGraphQLHandler } from '@redwoodjs/graphql-server' @@ -469,7 +469,7 @@ export const handler = createGraphQLHandler({ " `; -exports[`authTasks addApiConfig Doesn't add authDecoder arg if one already exists, even with a non-standard import name and arg placement 1`] = ` +exports[`authTasks > addApiConfig > Doesn't add authDecoder arg if one already exists, even with a non-standard import name and arg placement 1`] = ` "import { authDecoder } from 'test-auth-api' import { createGraphQLHandler } from '@redwoodjs/graphql-server' @@ -496,6 +496,6 @@ export const handler = createGraphQLHandler({ " `; -exports[`authTasks writes an auth.js file for JS projects 1`] = `"// web auth template"`; +exports[`authTasks > writes an auth.js file for JS projects 1`] = `"// web auth template"`; -exports[`authTasks writes an auth.ts file for TS projects 1`] = `"// web auth template"`; +exports[`authTasks > writes an auth.ts file for TS projects 1`] = `"// web auth template"`; diff --git a/packages/cli-helpers/src/auth/__tests__/authFiles.test.ts b/packages/cli-helpers/src/auth/__tests__/authFiles.test.ts index b7ca9a3d778e..2c4f6fe395d5 100644 --- a/packages/cli-helpers/src/auth/__tests__/authFiles.test.ts +++ b/packages/cli-helpers/src/auth/__tests__/authFiles.test.ts @@ -1,13 +1,14 @@ // Have to use `var` here to avoid "Temporal Dead Zone" issues let mockBasePath = '' -let mockIsTypeScriptProject = true globalThis.__dirname = __dirname -jest.mock('../../lib/paths', () => { +vi.mock('../../lib/paths', async (importOriginal) => { const path = require('path') + // eslint-disable-next-line @typescript-eslint/consistent-type-imports + const orginalPaths = await importOriginal() return { - ...jest.requireActual('../../lib/paths'), + ...orginalPaths, getPaths: () => { const base = mockBasePath || '/mock/base/path' @@ -22,17 +23,20 @@ jest.mock('../../lib/paths', () => { } }) -jest.mock('../../lib/project', () => ({ - isTypeScriptProject: () => mockIsTypeScriptProject, +vi.mock('../../lib/project', () => ({ + isTypeScriptProject: vi.fn(), })) import path from 'path' +import { vi, beforeEach, it, expect } from 'vitest' + import { getPaths } from '../../lib/paths' +import { isTypeScriptProject } from '../../lib/project' import { apiSideFiles, generateUniqueFileNames } from '../authFiles' beforeEach(() => { - mockIsTypeScriptProject = true + vi.mocked(isTypeScriptProject).mockReturnValue(true) }) it('generates a record of TS files', () => { @@ -51,7 +55,7 @@ it('generates a record of TS files', () => { }) it('generates a record of JS files', () => { - mockIsTypeScriptProject = false + vi.mocked(isTypeScriptProject).mockReturnValue(false) const filePaths = Object.keys( apiSideFiles({ diff --git a/packages/cli-helpers/src/auth/__tests__/authTasks.test.ts b/packages/cli-helpers/src/auth/__tests__/authTasks.test.ts index 70f6b77603e8..6d7f007b506b 100644 --- a/packages/cli-helpers/src/auth/__tests__/authTasks.test.ts +++ b/packages/cli-helpers/src/auth/__tests__/authTasks.test.ts @@ -1,72 +1,67 @@ -// Have to use `var` here to avoid "Temporal Dead Zone" issues -// eslint-disable-next-line -var mockIsTypeScriptProject = true - -jest.mock('../../lib/project', () => ({ - isTypeScriptProject: () => mockIsTypeScriptProject, -})) - -jest.mock('../../lib', () => ({ +vi.mock('../../lib', () => ({ transformTSToJS: (_path: string, data: string) => data, })) // mock Telemetry for CLI commands so they don't try to spawn a process -jest.mock('@redwoodjs/telemetry', () => { +vi.mock('@redwoodjs/telemetry', () => { return { - errorTelemetry: () => jest.fn(), - timedTelemetry: () => jest.fn(), + errorTelemetry: () => vi.fn(), + timedTelemetry: () => vi.fn(), } }) -jest.mock('../../lib/paths', () => { - const path = require('path') - const actualPaths = jest.requireActual('../../lib/paths') - const basedir = '/mock/setup/path' - const app = mockIsTypeScriptProject ? 'App.tsx' : 'App.jsx' - const routes = mockIsTypeScriptProject ? 'Routes.tsx' : 'Routes.jsx' - +vi.mock('../../lib/paths', () => { return { - resolveFile: actualPaths.resolveFile, - getPaths: () => ({ - api: { - functions: '', - src: '', - lib: '', - graphql: path.join(basedir, 'api/src/functions/graphql.ts'), - }, - web: { - src: path.join(basedir, 'web/src'), - app: path.join(basedir, `web/src/${app}`), - routes: path.join(basedir, `web/src/${routes}`), - }, - base: path.join(basedir), - }), + getPaths: vi.fn(), } }) -jest.mock('../../lib/project', () => { +vi.mock('../../lib/project', async () => { + const { getPaths } = await import('../../lib/paths') return { - isTypeScriptProject: () => mockIsTypeScriptProject, + isTypeScriptProject: vi.fn(), getGraphqlPath: () => { - const { getPaths } = require('../../lib/paths') return getPaths().api.graphql }, } }) // This will load packages/cli-helpers/__mocks__/fs.js -jest.mock('fs') +vi.mock('fs') +vi.mock('node:fs', async () => { + const memfs = await import('memfs') + return { + ...memfs.fs, + default: memfs.fs, + } +}) -const mockFS = fs as unknown as Omit, 'readdirSync'> & { - __setMockFiles: (files: Record) => void - __getMockFiles: () => Record - readdirSync: () => string[] +const mockedPathGenerator = (app: string, routes: string) => { + const basedir = '/mock/setup/path' + return { + api: { + functions: '', + src: '', + lib: '', + graphql: path.join(basedir, 'api/src/functions/graphql.ts'), + }, + web: { + src: path.join(basedir, 'web/src'), + app: path.join(basedir, `web/src/${app}`), + routes: path.join(basedir, `web/src/${routes}`), + }, + base: path.join(basedir), + } } import fs from 'fs' import path from 'path' +import { vol } from 'memfs' +import { vi, beforeEach, describe, it, expect, test } from 'vitest' + import { getPaths } from '../../lib/paths' +import { isTypeScriptProject } from '../../lib/project' import type { AuthGeneratorCtx } from '../authTasks' import { addApiConfig, @@ -98,10 +93,14 @@ function platformPath(filePath: string) { } beforeEach(() => { - mockIsTypeScriptProject = true - jest.restoreAllMocks() - - mockFS.__setMockFiles({ + vi.restoreAllMocks() + vi.mocked(isTypeScriptProject).mockReturnValue(true) + vi.mocked(getPaths).mockReturnValue( + // @ts-expect-error - We are not returning a full set of mock paths here + mockedPathGenerator('App.tsx', 'Routes.tsx') + ) + + vol.fromJSON({ [path.join( getPaths().base, platformPath('/templates/web/auth.ts.template') @@ -110,10 +109,6 @@ beforeEach(() => { [getPaths().api.graphql]: graphqlTs, [getPaths().web.routes]: routesTsx, }) - - mockFS.readdirSync = () => { - return ['auth.ts.template'] - } }) describe('authTasks', () => { @@ -123,8 +118,8 @@ describe('authTasks', () => { platformPath('/templates/web/auth.ts.template') ) - mockFS.__setMockFiles({ - ...mockFS.__getMockFiles(), + vol.fromJSON({ + ...vol.toJSON(), [templatePath]: auth0WebAuthTsTemplate, }) @@ -139,9 +134,9 @@ describe('authTasks', () => { const authTsPath = path.join(getPaths().web.src, 'auth.ts') - expect(fs.readFileSync(getPaths().web.app)).toMatchSnapshot() - expect(fs.readFileSync(authTsPath)).toMatchSnapshot() - expect(fs.readFileSync(getPaths().web.routes)).toMatchSnapshot() + expect(fs.readFileSync(getPaths().web.app, 'utf-8')).toMatchSnapshot() + expect(fs.readFileSync(authTsPath, 'utf-8')).toMatchSnapshot() + expect(fs.readFileSync(getPaths().web.routes, 'utf-8')).toMatchSnapshot() }) it('Should update App.{jsx,tsx}, Routes.{jsx,tsx} and add auth.ts (Clerk)', () => { @@ -150,13 +145,18 @@ describe('authTasks', () => { platformPath('/templates/web/auth.tsx.template') ) - mockFS.__setMockFiles({ - ...mockFS.__getMockFiles(), + // NOTE: We reset here because we had to remove the `auth.ts.template` + // file that would be here as a result of the `beforeEach` above. + // The previous implementation of this test was mocking the `fs` module to + // return only `auth.tsx.template` and not the `auth.ts.template` file even + // though it was on the mock filesystem. + vol.reset() + vol.fromJSON({ + [getPaths().web.app]: webAppTsx, + [getPaths().api.graphql]: graphqlTs, + [getPaths().web.routes]: routesTsx, [templatePath]: clerkWebAuthTsTemplate, }) - mockFS.readdirSync = () => { - return ['auth.tsx.template'] - } const ctx: AuthGeneratorCtx = { provider: 'clerk', @@ -169,14 +169,14 @@ describe('authTasks', () => { const authTsPath = path.join(getPaths().web.src, 'auth.tsx') - expect(fs.readFileSync(getPaths().web.app)).toMatchSnapshot() - expect(fs.readFileSync(authTsPath)).toMatchSnapshot() - expect(fs.readFileSync(getPaths().web.routes)).toMatchSnapshot() + expect(fs.readFileSync(getPaths().web.app, 'utf-8')).toMatchSnapshot() + expect(fs.readFileSync(authTsPath, 'utf-8')).toMatchSnapshot() + expect(fs.readFileSync(getPaths().web.routes, 'utf-8')).toMatchSnapshot() }) it('Should update App.tsx for legacy apps', () => { - mockFS.__setMockFiles({ - ...mockFS.__getMockFiles(), + vol.fromJSON({ + ...vol.toJSON(), [getPaths().web.app]: legacyAuthWebAppTsx, }) @@ -187,13 +187,13 @@ describe('authTasks', () => { addConfigToWebApp().task(ctx, {} as any) - expect(fs.readFileSync(getPaths().web.app)).toMatchSnapshot() + expect(fs.readFileSync(getPaths().web.app, 'utf-8')).toMatchSnapshot() }) describe('Components with props', () => { it('Should add useAuth on the same line for single line components, and separate line for multiline components', () => { - mockFS.__setMockFiles({ - ...mockFS.__getMockFiles(), + vol.fromJSON({ + ...vol.toJSON(), [getPaths().web.app]: customApolloAppTsx, [getPaths().web.routes]: customPropsRoutesTsx, }) @@ -206,13 +206,13 @@ describe('authTasks', () => { addConfigToWebApp().task(ctx, {} as any) addConfigToRoutes().task() - expect(fs.readFileSync(getPaths().web.app)).toMatchSnapshot() - expect(fs.readFileSync(getPaths().web.routes)).toMatchSnapshot() + expect(fs.readFileSync(getPaths().web.app, 'utf-8')).toMatchSnapshot() + expect(fs.readFileSync(getPaths().web.routes, 'utf-8')).toMatchSnapshot() }) it('Should not add useAuth if one already exists', () => { - mockFS.__setMockFiles({ - ...mockFS.__getMockFiles(), + vol.fromJSON({ + ...vol.toJSON(), [getPaths().web.app]: customApolloAppTsx, [getPaths().web.routes]: useAuthRoutesTsx, }) @@ -225,15 +225,15 @@ describe('authTasks', () => { addConfigToWebApp().task(ctx, {} as any) addConfigToRoutes().task() - expect(fs.readFileSync(getPaths().web.app)).toMatchSnapshot() - expect(fs.readFileSync(getPaths().web.routes)).toMatchSnapshot() + expect(fs.readFileSync(getPaths().web.app, 'utf-8')).toMatchSnapshot() + expect(fs.readFileSync(getPaths().web.routes, 'utf-8')).toMatchSnapshot() }) }) describe('Customized App.js', () => { it('Should add auth config when using explicit return', () => { - mockFS.__setMockFiles({ - ...mockFS.__getMockFiles(), + vol.fromJSON({ + ...vol.toJSON(), [getPaths().web.app]: explicitReturnAppTsx, }) @@ -244,14 +244,14 @@ describe('authTasks', () => { addConfigToWebApp().task(ctx, {} as any) - expect(fs.readFileSync(getPaths().web.app)).toMatchSnapshot() + expect(fs.readFileSync(getPaths().web.app, 'utf-8')).toMatchSnapshot() }) }) describe('Swapped out GraphQL client', () => { it('Should add auth config when app is missing RedwoodApolloProvider', () => { - mockFS.__setMockFiles({ - ...mockFS.__getMockFiles(), + vol.fromJSON({ + ...vol.toJSON(), [getPaths().web.app]: withoutRedwoodApolloAppTsx, }) @@ -264,7 +264,7 @@ describe('authTasks', () => { addConfigToWebApp().task(ctx, task) expect(task.output).toMatch(/GraphQL.*useAuth/) - expect(fs.readFileSync(getPaths().web.app)).toMatchSnapshot() + expect(fs.readFileSync(getPaths().web.app, 'utf-8')).toMatchSnapshot() }) }) @@ -275,12 +275,12 @@ describe('authTasks', () => { authDecoderImport: "import { authDecoder } from 'test-auth-api'", }) - expect(fs.readFileSync(getPaths().api.graphql)).toMatchSnapshot() + expect(fs.readFileSync(getPaths().api.graphql, 'utf-8')).toMatchSnapshot() }) it("Doesn't add authDecoder arg if one already exists", () => { - mockFS.__setMockFiles({ - ...mockFS.__getMockFiles(), + vol.fromJSON({ + ...vol.toJSON(), [getPaths().api.graphql]: withAuthDecoderGraphqlTs, }) @@ -289,12 +289,12 @@ describe('authTasks', () => { authDecoderImport: "import { authDecoder } from 'test-auth-api'", }) - expect(fs.readFileSync(getPaths().api.graphql)).toMatchSnapshot() + expect(fs.readFileSync(getPaths().api.graphql, 'utf-8')).toMatchSnapshot() }) it("Doesn't add authDecoder arg if one already exists, even with a non-standard import name and arg placement", () => { - mockFS.__setMockFiles({ - ...mockFS.__getMockFiles(), + vol.fromJSON({ + ...vol.toJSON(), [getPaths().api.graphql]: nonStandardAuthDecoderGraphqlTs, }) @@ -303,7 +303,7 @@ describe('authTasks', () => { authDecoderImport: "import { authDecoder } from 'test-auth-api'", }) - expect(fs.readFileSync(getPaths().api.graphql)).toMatchSnapshot() + expect(fs.readFileSync(getPaths().api.graphql, 'utf-8')).toMatchSnapshot() }) }) @@ -632,18 +632,23 @@ describe('authTasks', () => { provider: 'auth0', setupMode: 'FORCE', } + + // NOTE: The current fs related mocking leaves this file around from previous tests so we + // must delete it here. This should be fixed in a future refactoring of the entire test suite + fs.rmSync(path.join(getPaths().base, 'templates/web/auth.tsx.template')) + createWebAuth(getPaths().base, false).task(ctx) expect( - fs.readFileSync(path.join(getPaths().web.src, 'auth.ts')) + fs.readFileSync(path.join(getPaths().web.src, 'auth.ts'), 'utf-8') ).toMatchSnapshot() }) it('writes an auth.js file for JS projects', () => { - mockIsTypeScriptProject = false + vi.mocked(isTypeScriptProject).mockReturnValue(false) - mockFS.__setMockFiles({ - ...mockFS.__getMockFiles(), + vol.fromJSON({ + ...vol.toJSON(), [getPaths().web.app]: webAppTsx, }) @@ -654,7 +659,7 @@ describe('authTasks', () => { createWebAuth(getPaths().base, false).task(ctx) expect( - fs.readFileSync(path.join(getPaths().web.src, 'auth.js')) + fs.readFileSync(path.join(getPaths().web.src, 'auth.js'), 'utf-8') ).toMatchSnapshot() }) }) diff --git a/packages/cli-helpers/src/auth/__tests__/setupHelpers.test.ts b/packages/cli-helpers/src/auth/__tests__/setupHelpers.test.ts index eba30cdb464a..b549baf7a916 100644 --- a/packages/cli-helpers/src/auth/__tests__/setupHelpers.test.ts +++ b/packages/cli-helpers/src/auth/__tests__/setupHelpers.test.ts @@ -1,14 +1,14 @@ globalThis.__dirname = __dirname // mock Telemetry for CLI commands so they don't try to spawn a process -jest.mock('@redwoodjs/telemetry', () => { +vi.mock('@redwoodjs/telemetry', () => { return { - errorTelemetry: () => jest.fn(), - timedTelemetry: () => jest.fn(), + errorTelemetry: () => vi.fn(), + timedTelemetry: () => vi.fn(), } }) -jest.mock('../../lib/paths', () => { +vi.mock('../../lib/paths', () => { const path = require('path') const __dirname = path.resolve() @@ -28,42 +28,45 @@ jest.mock('../../lib/paths', () => { } }) -jest.mock('../../lib/project', () => ({ +vi.mock('../../lib/project', () => ({ isTypeScriptProject: () => true, })) -jest.mock('execa', () => {}) -jest.mock('listr2') -jest.mock('prompts', () => jest.fn(() => ({ answer: true }))) +vi.mock('execa') +vi.mock('listr2') +vi.mock('prompts', () => ({ + default: vi.fn(() => ({ answer: true })), +})) import fs from 'fs' import path from 'path' import { Listr } from 'listr2' import prompts from 'prompts' +import { vi, describe, afterEach, it, expect } from 'vitest' // import * as auth from '../auth' import { standardAuthHandler } from '../setupHelpers' describe('Auth generator tests', () => { - const processExitSpy = jest + const processExitSpy = vi .spyOn(process, 'exit') .mockImplementation((_code: any) => {}) - const mockListrRun = jest.fn() + const mockListrRun = vi.fn() - ;(Listr as jest.MockedFunction).mockImplementation(() => { + ;(Listr as vi.MockedFunction).mockImplementation(() => { return { run: mockListrRun, } }) - const fsSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}) + const fsSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {}) afterEach(() => { processExitSpy.mockReset() fsSpy.mockReset() - ;(prompts as unknown as jest.Mock).mockClear() + ;(prompts as unknown as vi.Mock).mockClear() mockListrRun.mockClear() }) diff --git a/packages/cli-helpers/src/auth/authFiles.ts b/packages/cli-helpers/src/auth/authFiles.ts index 85bd5f812075..c64f48f8141a 100644 --- a/packages/cli-helpers/src/auth/authFiles.ts +++ b/packages/cli-helpers/src/auth/authFiles.ts @@ -3,9 +3,9 @@ import path from 'path' import pascalcase from 'pascalcase' -import { transformTSToJS } from '../lib' -import { getPaths } from '../lib/paths' -import { isTypeScriptProject } from '../lib/project' +import { transformTSToJS } from '../lib/index.js' +import { getPaths } from '../lib/paths.js' +import { isTypeScriptProject } from '../lib/project.js' interface FilesArgs { basedir: string diff --git a/packages/cli-helpers/src/auth/authTasks.ts b/packages/cli-helpers/src/auth/authTasks.ts index 69d19239cb73..c4d515c7e6d2 100644 --- a/packages/cli-helpers/src/auth/authTasks.ts +++ b/packages/cli-helpers/src/auth/authTasks.ts @@ -5,17 +5,17 @@ import type { ListrRenderer, ListrTask, ListrTaskWrapper } from 'listr2' import { resolveFile } from '@redwoodjs/project-config' -import type { ExistingFiles } from '../lib' -import { transformTSToJS, writeFilesTask } from '../lib' -import { colors } from '../lib/colors' -import { getPaths } from '../lib/paths' +import { colors } from '../lib/colors.js' +import type { ExistingFiles } from '../lib/index.js' +import { transformTSToJS, writeFilesTask } from '../lib/index.js' +import { getPaths } from '../lib/paths.js' import { getGraphqlPath, graphFunctionDoesExist, isTypeScriptProject, -} from '../lib/project' +} from '../lib/project.js' -import { apiSideFiles, generateUniqueFileNames } from './authFiles' +import { apiSideFiles, generateUniqueFileNames } from './authFiles.js' const AUTH_PROVIDER_HOOK_IMPORT = `import { AuthProvider, useAuth } from './auth'` const AUTH_HOOK_IMPORT = `import { useAuth } from './auth'` diff --git a/packages/cli-helpers/src/auth/setupHelpers.ts b/packages/cli-helpers/src/auth/setupHelpers.ts index 5c572a7c6427..0daaeae51e3a 100644 --- a/packages/cli-helpers/src/auth/setupHelpers.ts +++ b/packages/cli-helpers/src/auth/setupHelpers.ts @@ -1,18 +1,18 @@ import type { ListrTask } from 'listr2' import { Listr } from 'listr2' import terminalLink from 'terminal-link' -import type yargs from 'yargs' +import type { Argv } from 'yargs' import { errorTelemetry } from '@redwoodjs/telemetry' -import { colors } from '../lib/colors' +import { colors } from '../lib/colors.js' import { addApiPackages, addWebPackages, installPackages, -} from '../lib/installHelpers' +} from '../lib/installHelpers.js' -import type { AuthGeneratorCtx } from './authTasks' +import type { AuthGeneratorCtx } from './authTasks.js' import { addAuthConfigToGqlApi, addConfigToRoutes, @@ -20,9 +20,9 @@ import { setAuthSetupMode, createWebAuth, generateAuthApiFiles, -} from './authTasks' +} from './authTasks.js' -export const standardAuthBuilder = (yargs: yargs.Argv) => { +export const standardAuthBuilder = (yargs: Argv) => { return yargs .option('force', { alias: 'f', diff --git a/packages/cli-helpers/src/index.ts b/packages/cli-helpers/src/index.ts index 04e8c7a96987..35f5d335f459 100644 --- a/packages/cli-helpers/src/index.ts +++ b/packages/cli-helpers/src/index.ts @@ -1,13 +1,13 @@ // @WARN: This export is going to cause memory problems in the CLI. // We need to split this into smaller packages, or use export aliasing (like in packages/testing/cache) -export * from './lib' -export * from './lib/colors' -export * from './lib/paths' -export * from './lib/project' -export * from './lib/version' -export * from './auth/setupHelpers' +export * from './lib/index.js' +export * from './lib/colors.js' +export * from './lib/paths.js' +export * from './lib/project.js' +export * from './lib/version.js' +export * from './auth/setupHelpers.js' -export * from './lib/installHelpers' +export * from './lib/installHelpers.js' -export * from './telemetry/index' +export * from './telemetry/index.js' diff --git a/packages/cli-helpers/src/lib/__tests__/__snapshots__/index.test.ts.snap b/packages/cli-helpers/src/lib/__tests__/__snapshots__/index.test.ts.snap index 1b01f64e6fa0..63ae56f46b8b 100644 --- a/packages/cli-helpers/src/lib/__tests__/__snapshots__/index.test.ts.snap +++ b/packages/cli-helpers/src/lib/__tests__/__snapshots__/index.test.ts.snap @@ -1,4 +1,4 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`prettify formats tsx content 1`] = ` "import React from 'react' diff --git a/packages/cli-helpers/src/lib/__tests__/__snapshots__/project.test.ts.snap b/packages/cli-helpers/src/lib/__tests__/__snapshots__/project.test.ts.snap index 6da535eaf61e..c3d1be489a33 100644 --- a/packages/cli-helpers/src/lib/__tests__/__snapshots__/project.test.ts.snap +++ b/packages/cli-helpers/src/lib/__tests__/__snapshots__/project.test.ts.snap @@ -1,6 +1,6 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`addEnvVar addEnvVar adds environment variables as part of a setup task should add a comment that the existing environment variable value was not changed, but include its new value as a comment 1`] = ` +exports[`addEnvVar > addEnvVar adds environment variables as part of a setup task > should add a comment that the existing environment variable value was not changed, but include its new value as a comment 1`] = ` "EXISTING_VAR=value # CommentedVar=123 @@ -10,7 +10,7 @@ exports[`addEnvVar addEnvVar adds environment variables as part of a setup task " `; -exports[`addEnvVar addEnvVar adds environment variables as part of a setup task should add a new environment variable when it does not exist 1`] = ` +exports[`addEnvVar > addEnvVar adds environment variables as part of a setup task > should add a new environment variable when it does not exist 1`] = ` "EXISTING_VAR = value # CommentedVar = 123 @@ -19,7 +19,7 @@ NEW_VAR = new_value " `; -exports[`addEnvVar addEnvVar adds environment variables as part of a setup task should add a new environment variable when it does not exist when existing envars have no spacing 1`] = ` +exports[`addEnvVar > addEnvVar adds environment variables as part of a setup task > should add a new environment variable when it does not exist when existing envars have no spacing 1`] = ` "EXISTING_VAR=value # CommentedVar = 123 @@ -28,7 +28,7 @@ NEW_VAR = new_value " `; -exports[`addEnvVar addEnvVar adds environment variables as part of a setup task should handle existing environment variables and new value with quoted values by not updating the original value 1`] = ` +exports[`addEnvVar > addEnvVar adds environment variables as part of a setup task > should handle existing environment variables and new value with quoted values by not updating the original value 1`] = ` "EXISTING_VAR = "value" # CommentedVar = 123 @@ -38,19 +38,19 @@ exports[`addEnvVar addEnvVar adds environment variables as part of a setup task " `; -exports[`addEnvVar addEnvVar adds environment variables as part of a setup task should handle existing environment variables with quoted values 1`] = ` +exports[`addEnvVar > addEnvVar adds environment variables as part of a setup task > should handle existing environment variables with quoted values 1`] = ` "EXISTING_VAR = "value" # CommentedVar = 123 " `; -exports[`addEnvVar addEnvVar adds environment variables as part of a setup task should handle existing environment variables with quoted values and no spacing 1`] = ` +exports[`addEnvVar > addEnvVar adds environment variables as part of a setup task > should handle existing environment variables with quoted values and no spacing 1`] = ` "EXISTING_VAR="value" # CommentedVar=123 " `; -exports[`updateTomlConfig updateTomlConfig configures a new CLI plugin adds package but keeps autoInstall false 1`] = ` +exports[`updateTomlConfig > updateTomlConfig configures a new CLI plugin > adds package but keeps autoInstall false 1`] = ` "[web] title = "Redwood App" port = 8_910 @@ -69,7 +69,7 @@ enabled = true " `; -exports[`updateTomlConfig updateTomlConfig configures a new CLI plugin adds when experimental cli has some plugins configured 1`] = ` +exports[`updateTomlConfig > updateTomlConfig configures a new CLI plugin > adds when experimental cli has some plugins configured 1`] = ` "[web] title = "Redwood App" port = 8_910 @@ -91,7 +91,7 @@ enabled = true " `; -exports[`updateTomlConfig updateTomlConfig configures a new CLI plugin adds when experimental cli is not configured 1`] = ` +exports[`updateTomlConfig > updateTomlConfig configures a new CLI plugin > adds when experimental cli is not configured 1`] = ` "[web] title = "Redwood App" port = 8_910 @@ -110,7 +110,7 @@ autoInstall = true " `; -exports[`updateTomlConfig updateTomlConfig configures a new CLI plugin adds when experimental cli is setup but has no plugins configured 1`] = ` +exports[`updateTomlConfig > updateTomlConfig configures a new CLI plugin > adds when experimental cli is setup but has no plugins configured 1`] = ` "[web] title = "Redwood App" port = 8_910 @@ -129,7 +129,7 @@ enabled = true " `; -exports[`updateTomlConfig updateTomlConfig configures a new CLI plugin does not add duplicate place when experimental cli has that plugin configured 1`] = ` +exports[`updateTomlConfig > updateTomlConfig configures a new CLI plugin > does not add duplicate place when experimental cli has that plugin configured 1`] = ` "[web] title = "Redwood App" port = 8_910 diff --git a/packages/cli-helpers/src/lib/__tests__/index.test.ts b/packages/cli-helpers/src/lib/__tests__/index.test.ts index 3fae865c059f..3c9c608c1734 100644 --- a/packages/cli-helpers/src/lib/__tests__/index.test.ts +++ b/packages/cli-helpers/src/lib/__tests__/index.test.ts @@ -1,6 +1,8 @@ +import { vi, test, expect } from 'vitest' + import { prettify } from '../index' -jest.mock('../paths', () => { +vi.mock('../paths', () => { return { getPaths: () => { return { diff --git a/packages/cli-helpers/src/lib/__tests__/project.addTomlSetting.test.ts b/packages/cli-helpers/src/lib/__tests__/project.addTomlSetting.test.ts index a6ed682047e3..c46185cf261f 100644 --- a/packages/cli-helpers/src/lib/__tests__/project.addTomlSetting.test.ts +++ b/packages/cli-helpers/src/lib/__tests__/project.addTomlSetting.test.ts @@ -1,7 +1,14 @@ -jest.mock('fs', () => require('memfs').fs) -jest.mock('node:fs', () => require('memfs').fs) +vi.mock('fs') +vi.mock('node:fs', async () => { + const memfs = await import('memfs') + return { + ...memfs.fs, + default: memfs.fs, + } +}) import { vol } from 'memfs' +import { vi, beforeAll, afterAll, it, expect } from 'vitest' import { setTomlSetting } from '../project' @@ -16,8 +23,8 @@ beforeAll(() => { afterAll(() => { process.env.RWJS_CWD = original_RWJS_CWD - jest.restoreAllMocks() - jest.resetModules() + vi.restoreAllMocks() + vi.resetModules() }) it('should add `fragments = true` to empty redwood.toml', () => { diff --git a/packages/cli-helpers/src/lib/__tests__/project.test.ts b/packages/cli-helpers/src/lib/__tests__/project.test.ts index a8aa58145848..f5e45301e0ef 100644 --- a/packages/cli-helpers/src/lib/__tests__/project.test.ts +++ b/packages/cli-helpers/src/lib/__tests__/project.test.ts @@ -1,9 +1,16 @@ -jest.mock('fs') -jest.mock('node:fs') +vi.mock('fs') +vi.mock('node:fs', async () => { + const memfs = await import('memfs') + return { + ...memfs.fs, + default: memfs.fs, + } +}) import * as fs from 'node:fs' import * as toml from '@iarna/toml' +import { vi, describe, beforeEach, afterEach, it, expect } from 'vitest' import { updateTomlConfig, addEnvVar } from '../project' @@ -23,7 +30,7 @@ const getRedwoodToml = () => { return defaultRedwoodToml } -jest.mock('@redwoodjs/project-config', () => { +vi.mock('@redwoodjs/project-config', () => { return { getPaths: () => { return { @@ -47,22 +54,22 @@ describe('addEnvVar', () => { describe('addEnvVar adds environment variables as part of a setup task', () => { beforeEach(() => { - jest.spyOn(fs, 'existsSync').mockImplementation(() => { + vi.spyOn(fs, 'existsSync').mockImplementation(() => { return true }) - jest.spyOn(fs, 'readFileSync').mockImplementation(() => { + vi.spyOn(fs, 'readFileSync').mockImplementation(() => { return envFileContent }) - jest.spyOn(fs, 'writeFileSync').mockImplementation((envPath, envFile) => { + vi.spyOn(fs, 'writeFileSync').mockImplementation((envPath, envFile) => { expect(envPath).toContain('.env') return envFile }) }) afterEach(() => { - jest.restoreAllMocks() + vi.restoreAllMocks() envFileContent = '' }) @@ -121,24 +128,22 @@ describe('addEnvVar', () => { describe('updateTomlConfig', () => { describe('updateTomlConfig configures a new CLI plugin', () => { beforeEach(() => { - jest.spyOn(fs, 'existsSync').mockImplementation(() => { + vi.spyOn(fs, 'existsSync').mockImplementation(() => { return true }) - jest.spyOn(fs, 'readFileSync').mockImplementation(() => { + vi.spyOn(fs, 'readFileSync').mockImplementation(() => { return toml.stringify(defaultRedwoodToml) }) - jest - .spyOn(fs, 'writeFileSync') - .mockImplementation((tomlPath, tomlFile) => { - expect(tomlPath).toContain('redwood.toml') - return tomlFile - }) + vi.spyOn(fs, 'writeFileSync').mockImplementation((tomlPath, tomlFile) => { + expect(tomlPath).toContain('redwood.toml') + return tomlFile + }) }) afterEach(() => { - jest.restoreAllMocks() + vi.restoreAllMocks() }) it('adds when experimental cli is not configured', () => { diff --git a/packages/cli-helpers/src/lib/__tests__/version.test.ts b/packages/cli-helpers/src/lib/__tests__/version.test.ts index cbcd8bb43e68..057e2463ccf8 100644 --- a/packages/cli-helpers/src/lib/__tests__/version.test.ts +++ b/packages/cli-helpers/src/lib/__tests__/version.test.ts @@ -1,4 +1,4 @@ -jest.mock('@redwoodjs/project-config', () => { +vi.mock('@redwoodjs/project-config', () => { return { getPaths: () => { return { @@ -7,10 +7,12 @@ jest.mock('@redwoodjs/project-config', () => { }, } }) -jest.mock('fs') +vi.mock('fs') import fs from 'fs' +import { vi, describe, test, expect, beforeEach } from 'vitest' + import { getCompatibilityData } from '../version' const EXAMPLE_PACKUMENT = { @@ -187,7 +189,7 @@ const EXAMPLE_PACKUMENT = { describe('version compatibility detection', () => { beforeEach(() => { - jest.spyOn(global, 'fetch').mockImplementation(() => { + vi.spyOn(global, 'fetch').mockImplementation(() => { return { json: () => { return EXAMPLE_PACKUMENT @@ -195,7 +197,7 @@ describe('version compatibility detection', () => { } as any }) - jest.spyOn(fs, 'readFileSync').mockImplementation(() => { + vi.spyOn(fs, 'readFileSync').mockImplementation(() => { return JSON.stringify({ devDependencies: { '@redwoodjs/core': '^6.0.0', @@ -206,15 +208,17 @@ describe('version compatibility detection', () => { test('throws for some fetch related error', async () => { // Mock the fetch function to throw an error - jest.spyOn(global, 'fetch').mockImplementation(() => { + vi.spyOn(global, 'fetch').mockImplementation(() => { throw new Error('Some fetch related error') }) await expect( getCompatibilityData('some-package', 'latest') - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Some fetch related error"`) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Some fetch related error]` + ) // Mock the json parsing to throw an error - jest.spyOn(global, 'fetch').mockImplementation(() => { + vi.spyOn(global, 'fetch').mockImplementation(() => { return { json: () => { throw new Error('Some json parsing error') @@ -224,11 +228,13 @@ describe('version compatibility detection', () => { await expect( getCompatibilityData('some-package', 'latest') - ).rejects.toThrowErrorMatchingInlineSnapshot(`"Some json parsing error"`) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `[Error: Some json parsing error]` + ) }) test('throws for some packument related error', async () => { - jest.spyOn(global, 'fetch').mockImplementation(() => { + vi.spyOn(global, 'fetch').mockImplementation(() => { return { json: () => { return { @@ -241,7 +247,7 @@ describe('version compatibility detection', () => { await expect( getCompatibilityData('some-package', 'latest') ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Some packument related error"` + `[Error: Some packument related error]` ) }) @@ -249,7 +255,7 @@ describe('version compatibility detection', () => { await expect( getCompatibilityData('@scope/package-name', '0.0.4') ).rejects.toThrowErrorMatchingInlineSnapshot( - `"The package '@scope/package-name' does not have a version '0.0.4'"` + `[Error: The package '@scope/package-name' does not have a version '0.0.4']` ) }) @@ -257,12 +263,12 @@ describe('version compatibility detection', () => { await expect( getCompatibilityData('@scope/package-name', 'next') ).rejects.toThrowErrorMatchingInlineSnapshot( - `"The package '@scope/package-name' does not have a tag 'next'"` + `[Error: The package '@scope/package-name' does not have a tag 'next']` ) }) test('throws if no latest version could be found', async () => { - jest.spyOn(global, 'fetch').mockImplementation(() => { + vi.spyOn(global, 'fetch').mockImplementation(() => { return { json: () => { return { @@ -276,7 +282,7 @@ describe('version compatibility detection', () => { await expect( getCompatibilityData('@scope/package-name', 'latest') ).rejects.toThrowErrorMatchingInlineSnapshot( - `"The package '@scope/package-name' does not have a tag 'latest'"` + `[Error: The package '@scope/package-name' does not have a tag 'latest']` ) }) @@ -320,7 +326,7 @@ describe('version compatibility detection', () => { } ) - jest.spyOn(fs, 'readFileSync').mockImplementation(() => { + vi.spyOn(fs, 'readFileSync').mockImplementation(() => { return JSON.stringify({ devDependencies: { '@redwoodjs/core': '5.2.0', @@ -343,7 +349,7 @@ describe('version compatibility detection', () => { }) test('throws if no compatible version could be found', async () => { - jest.spyOn(fs, 'readFileSync').mockImplementation(() => { + vi.spyOn(fs, 'readFileSync').mockImplementation(() => { return JSON.stringify({ devDependencies: { '@redwoodjs/core': '7.0.0', @@ -354,7 +360,7 @@ describe('version compatibility detection', () => { expect( getCompatibilityData('@scope/package-name', 'latest') ).rejects.toThrowErrorMatchingInlineSnapshot( - `"No compatible version of '@scope/package-name' was found"` + `[Error: No compatible version of '@scope/package-name' was found]` ) }) }) diff --git a/packages/cli-helpers/src/lib/index.ts b/packages/cli-helpers/src/lib/index.ts index 1b294a27f9e5..1bc77ed61bea 100644 --- a/packages/cli-helpers/src/lib/index.ts +++ b/packages/cli-helpers/src/lib/index.ts @@ -10,8 +10,8 @@ import type { import { Listr } from 'listr2' import { format } from 'prettier' -import { colors } from './colors' -import { getPaths } from './paths' +import { colors } from './colors.js' +import { getPaths } from './paths.js' // TODO: Move this into `generateTemplate` when all templates have TS support /* diff --git a/packages/cli-helpers/src/lib/installHelpers.ts b/packages/cli-helpers/src/lib/installHelpers.ts index c8016953dee1..f70ae4e4c36f 100644 --- a/packages/cli-helpers/src/lib/installHelpers.ts +++ b/packages/cli-helpers/src/lib/installHelpers.ts @@ -1,6 +1,6 @@ import execa from 'execa' -import { getPaths } from './paths' +import { getPaths } from './paths.js' export const addWebPackages = (webPackages: string[]) => ({ title: 'Adding required web packages...', diff --git a/packages/cli-helpers/src/lib/paths.ts b/packages/cli-helpers/src/lib/paths.ts index 1f026d6ebc5c..7ad0f79cf755 100644 --- a/packages/cli-helpers/src/lib/paths.ts +++ b/packages/cli-helpers/src/lib/paths.ts @@ -1,6 +1,6 @@ import { getPaths as _getPaths } from '@redwoodjs/project-config' -import { colors } from './colors' +import { colors } from './colors.js' function isErrorWithMessage(e: any): e is { message: string } { return !!e.message diff --git a/packages/cli-helpers/src/lib/project.ts b/packages/cli-helpers/src/lib/project.ts index f1cd4d2b7225..8c79ffff5d29 100644 --- a/packages/cli-helpers/src/lib/project.ts +++ b/packages/cli-helpers/src/lib/project.ts @@ -13,8 +13,8 @@ import { resolveFile, } from '@redwoodjs/project-config' -import { colors } from './colors' -import { getPaths } from './paths' +import { colors } from './colors.js' +import { getPaths } from './paths.js' export const getGraphqlPath = () => { return resolveFile(path.join(getPaths().api.functions, 'graphql')) diff --git a/packages/cli-helpers/tsconfig.json b/packages/cli-helpers/tsconfig.json index 5e7f7fd919f2..e0e245aba844 100644 --- a/packages/cli-helpers/tsconfig.json +++ b/packages/cli-helpers/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../tsconfig.compilerOption.json", "compilerOptions": { - "strict": true, + "moduleResolution": "NodeNext", + "module": "NodeNext", "baseUrl": ".", "rootDir": "src", "outDir": "dist" diff --git a/packages/cli-helpers/vitest.config.mts b/packages/cli-helpers/vitest.config.mts new file mode 100644 index 000000000000..55b4842e1875 --- /dev/null +++ b/packages/cli-helpers/vitest.config.mts @@ -0,0 +1,7 @@ +import { defineConfig, configDefaults } from 'vitest/config' + +export default defineConfig({ + test: { + exclude: [...configDefaults.exclude, '**/fixtures', '**/mockFsFiles'], + }, +}) diff --git a/packages/project-config/tsconfig.json b/packages/project-config/tsconfig.json index 78a31c197ddd..23e3bf19df5b 100644 --- a/packages/project-config/tsconfig.json +++ b/packages/project-config/tsconfig.json @@ -7,5 +7,5 @@ "rootDir": "src", "outDir": "dist", }, - "include": ["src/**/*"], + "include": ["src"], } diff --git a/yarn.lock b/yarn.lock index 6f8001bd4a4e..a9628c17d8eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8097,9 +8097,7 @@ __metadata: version: 0.0.0-use.local resolution: "@redwoodjs/cli-helpers@workspace:packages/cli-helpers" dependencies: - "@babel/cli": "npm:7.23.4" "@babel/core": "npm:^7.22.20" - "@babel/runtime-corejs3": "npm:7.23.6" "@iarna/toml": "npm:2.2.5" "@opentelemetry/api": "npm:1.7.0" "@redwoodjs/project-config": "npm:6.0.7" @@ -8108,10 +8106,8 @@ __metadata: "@types/pascalcase": "npm:1.0.3" "@types/yargs": "npm:17.0.32" chalk: "npm:4.1.2" - core-js: "npm:3.34.0" dotenv: "npm:16.3.1" execa: "npm:5.1.1" - jest: "npm:29.7.0" listr2: "npm:6.6.1" lodash: "npm:4.17.21" pascalcase: "npm:1.0.0" @@ -8119,6 +8115,7 @@ __metadata: prompts: "npm:2.4.2" terminal-link: "npm:2.1.1" typescript: "npm:5.3.3" + vitest: "npm:1.2.1" languageName: unknown linkType: soft