diff --git a/GEMINI.md b/GEMINI.md index b7d0227716..41c2f79982 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -71,7 +71,8 @@ To test framework changes against a real Cedar project: - **Code Style:** - **Linting:** ESLint (`eslint.config.mjs`). - **Formatting:** Prettier. - - **TypeScript:** Never use `@ts-ignore`. Always prefer `@ts-expect-error` with a clear explanation for why it's needed. + - **TypeScript:** Never use `@ts-ignore`. Always prefer `@ts-expect-error` with a clear explanation for why it's needed. Avoid `any`. Use `unknown` or specific types whenever possible. If `any` is truly necessary, add a comment explaining why. `any` is more acceptable in test files than in implementation files. Avoid `as unknown as X`. + - **Comments:** Place comments on a separate line immediately above the affected code using `//`. Unless you're documenting the usage of a function or variable, in which case you should use JSDoc comments. Avoid in-line or end-of-line comments. - **Constraints:** Yarn constraints ensure consistent dependency versions across the monorepo. - **Testing:** - Unit tests: Jest/Vitest. When running `vitest` directly, use `--run` to disable watch mode. diff --git a/packages/cli/package.json b/packages/cli/package.json index f04e773c93..e089fdaeb3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -21,6 +21,7 @@ "scripts": { "build": "tsx ./build.mts", "build:pack": "yarn pack -o cedarjs-cli.tgz", + "build:types": "tsc --build --verbose ./tsconfig.build.json", "build:watch": "nodemon --watch src --ext \"js,jsx,ts,tsx,template\" --ignore dist --exec \"yarn build && yarn fix:permissions\"", "dev": "RWJS_CWD=../../__fixtures__/example-todo-main node dist/index.js", "fix:permissions": "chmod +x dist/index.js dist/cfw.js", diff --git a/packages/cli/src/commands/__tests__/build.test.js b/packages/cli/src/commands/__tests__/build.test.ts similarity index 77% rename from packages/cli/src/commands/__tests__/build.test.js rename to packages/cli/src/commands/__tests__/build.test.ts index 270185e454..6788c858ee 100644 --- a/packages/cli/src/commands/__tests__/build.test.js +++ b/packages/cli/src/commands/__tests__/build.test.ts @@ -1,5 +1,15 @@ +import type FS from 'node:fs' + +import { Listr } from 'listr2' +import { vi, afterEach, test, expect } from 'vitest' + +import type * as ProjectConfig from '@cedarjs/project-config' + +vi.mock('listr2') + +// Make sure prerender doesn't get triggered vi.mock('@cedarjs/project-config', async (importOriginal) => { - const originalProjectConfig = await importOriginal() + const originalProjectConfig = await importOriginal() return { ...originalProjectConfig, getPaths: () => { @@ -23,14 +33,14 @@ vi.mock('@cedarjs/project-config', async (importOriginal) => { } }) -vi.mock('node:fs', async () => { - const actualFs = await vi.importActual('node:fs') +vi.mock('node:fs', async (importOriginal) => { + const actualFs = await importOriginal() return { default: { ...actualFs, // Mock the existence of the Prisma config file - existsSync: (path) => { + existsSync: (path: string) => { if (path === '/mocked/project/api/prisma.config.js') { return true } @@ -41,12 +51,6 @@ vi.mock('node:fs', async () => { } }) -import { Listr } from 'listr2' -import { vi, afterEach, test, expect } from 'vitest' - -vi.mock('listr2') - -// Make sure prerender doesn't get triggered vi.mock('execa', () => ({ default: vi.fn((cmd, params) => ({ cmd, @@ -62,7 +66,8 @@ afterEach(() => { test('the build tasks are in the correct sequence', async () => { await handler({}) - expect(Listr.mock.calls[0][0].map((x) => x.title)).toMatchInlineSnapshot(` + const callArgs = vi.mocked(Listr).mock.calls[0][0] as { title: string }[] + expect(callArgs.map((x) => x.title)).toMatchInlineSnapshot(` [ "Generating Prisma Client...", "Verifying graphql schema...", @@ -80,7 +85,8 @@ test('Should run prerender for web', async () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) await handler({ side: ['web'], prerender: true }) - expect(Listr.mock.calls[0][0].map((x) => x.title)).toMatchInlineSnapshot(` + const callArgs = vi.mocked(Listr).mock.calls[0][0] as { title: string }[] + expect(callArgs.map((x) => x.title)).toMatchInlineSnapshot(` [ "Building Web...", ] diff --git a/packages/cli/src/commands/__tests__/exec.test.ts b/packages/cli/src/commands/__tests__/exec.test.ts index 13f7421f0c..faf696ef74 100644 --- a/packages/cli/src/commands/__tests__/exec.test.ts +++ b/packages/cli/src/commands/__tests__/exec.test.ts @@ -6,7 +6,6 @@ import { vi, afterEach, beforeEach, describe, it, expect } from 'vitest' // @ts-expect-error - No types for .js files import { runScriptFunction } from '../../lib/exec.js' import '../../lib/mockTelemetry' -// @ts-expect-error - No types for .js files import { handler } from '../execHandler.js' vi.mock('@cedarjs/babel-config', () => ({ diff --git a/packages/cli/src/commands/__tests__/info.test.js b/packages/cli/src/commands/__tests__/info.test.ts similarity index 100% rename from packages/cli/src/commands/__tests__/info.test.js rename to packages/cli/src/commands/__tests__/info.test.ts diff --git a/packages/cli/src/commands/__tests__/prisma.test.js b/packages/cli/src/commands/__tests__/prisma.test.ts similarity index 81% rename from packages/cli/src/commands/__tests__/prisma.test.js rename to packages/cli/src/commands/__tests__/prisma.test.ts index e0095fc9b6..dc57e1ff90 100644 --- a/packages/cli/src/commands/__tests__/prisma.test.js +++ b/packages/cli/src/commands/__tests__/prisma.test.ts @@ -1,5 +1,12 @@ +import fs from 'node:fs' + +import execa from 'execa' +import { vi, beforeEach, afterEach, test, expect } from 'vitest' + +import type * as ProjectConfig from '@cedarjs/project-config' + vi.mock('@cedarjs/project-config', async (importOriginal) => { - const originalProjectConfig = await importOriginal() + const originalProjectConfig = await importOriginal() return { ...originalProjectConfig, getPaths: () => { @@ -26,11 +33,6 @@ vi.mock('execa', () => ({ }, })) -import fs from 'node:fs' - -import execa from 'execa' -import { vi, beforeEach, afterEach, test, expect } from 'vitest' - import { handler } from '../prisma.js' beforeEach(() => { @@ -40,8 +42,8 @@ beforeEach(() => { }) afterEach(() => { - console.info.mockRestore() - console.log.mockRestore() + vi.mocked(console).info.mockRestore() + vi.mocked(console).log.mockRestore() }) test('the prisma command handles spaces', async () => { @@ -53,7 +55,7 @@ test('the prisma command handles spaces', async () => { n: 'add bazingas', }) - expect(execa.sync.mock.calls[0][1]).toEqual([ + expect(vi.mocked(execa.sync).mock.calls[0][1]).toEqual([ 'migrate', 'dev', '-n', diff --git a/packages/cli/src/commands/__tests__/test.test.js b/packages/cli/src/commands/__tests__/test.test.js deleted file mode 100644 index 1c8257c809..0000000000 --- a/packages/cli/src/commands/__tests__/test.test.js +++ /dev/null @@ -1,145 +0,0 @@ -globalThis.__dirname = __dirname -import '../../lib/test.js' - -vi.mock('execa', () => ({ - default: vi.fn((cmd, params) => ({ - cmd, - params, - })), -})) - -import fs from 'node:fs' - -import execa from 'execa' -import { vi, afterEach, test, expect, beforeEach } from 'vitest' - -import { handler } from '../test.js' - -vi.mock('@cedarjs/structure', () => { - return { - getProject: () => ({ - sides: ['web', 'api'], - }), - } -}) - -beforeEach(() => { - vi.spyOn(fs, 'existsSync').mockReturnValue(true) -}) - -afterEach(() => { - vi.clearAllMocks() -}) - -test('Runs tests for all available sides if no filter passed', async () => { - await handler({}) - - expect(execa.mock.results[0].value.cmd).toBe('yarn') - expect(execa.mock.results[0].value.params).toContain('jest') - expect(execa.mock.results[0].value.params).toContain('web') - expect(execa.mock.results[0].value.params).toContain('api') -}) - -test('Syncs or creates test database when the flag --db-push is set to true', async () => { - await handler({ - filter: ['api'], - dbPush: true, - }) - - expect(execa.mock.results[0].value.cmd).toBe('yarn') - expect(execa.mock.results[0].value.params).toContain('jest') - expect(execa.mock.results[0].value.params).toContain('--projects') - expect(execa.mock.results[0].value.params).toContain('api') -}) - -test('Skips test database sync/creation when the flag --db-push is set to false', async () => { - await handler({ - filter: ['api'], - dbPush: false, - }) - - expect(execa.mock.results[0].value.cmd).toBe('yarn') - expect(execa.mock.results[0].value.params).toContain('jest') -}) - -test('Runs tests for all available sides if no side filter passed', async () => { - await handler({ - filter: ['bazinga'], - }) - - expect(execa.mock.results[0].value.cmd).toBe('yarn') - expect(execa.mock.results[0].value.params).toContain('jest') - expect(execa.mock.results[0].value.params).toContain('bazinga') - expect(execa.mock.results[0].value.params).toContain('web') - expect(execa.mock.results[0].value.params).toContain('api') -}) - -test('Runs tests specified side if even with additional filters', async () => { - await handler({ - filter: ['web', 'bazinga'], - }) - - expect(execa.mock.results[0].value.cmd).not.toBe('yarn rw') - expect(execa.mock.results[0].value.params).not.toContain('api') - - expect(execa.mock.results[0].value.cmd).toBe('yarn') - expect(execa.mock.results[0].value.params).toContain('jest') - expect(execa.mock.results[0].value.params).toContain('bazinga') - expect(execa.mock.results[0].value.params).toContain('web') -}) - -test('Does not create db when calling test with just web', async () => { - await handler({ - filter: ['web'], - }) - - expect(execa.mock.results[0].value.cmd).toBe('yarn') - expect(execa.mock.results[0].value.params).toContain('jest') -}) - -test('Passes filter param to jest command if passed', async () => { - await handler({ - filter: ['web', 'bazinga'], - }) - - expect(execa.mock.results[0].value.cmd).toBe('yarn') - expect(execa.mock.results[0].value.params).toContain('jest') - expect(execa.mock.results[0].value.params).toContain('bazinga') -}) - -test('Passes other flags to jest', async () => { - await handler({ - u: true, - debug: true, - json: true, - collectCoverage: true, - }) - - expect(execa.mock.results[0].value.cmd).toBe('yarn') - expect(execa.mock.results[0].value.params).toContain('jest') - expect(execa.mock.results[0].value.params).toContain('-u') - expect(execa.mock.results[0].value.params).toContain('--debug') - expect(execa.mock.results[0].value.params).toContain('--json') - expect(execa.mock.results[0].value.params).toContain('--collectCoverage') -}) - -test('Passes values of other flags to jest', async () => { - await handler({ - bazinga: false, - hello: 'world', - }) - - // Second command because api side runs - expect(execa.mock.results[0].value.cmd).toBe('yarn') - expect(execa.mock.results[0].value.params).toContain('jest') - - // Note that these below tests aren't the best, since they don't check for order - // But I'm making sure only 2 extra params get passed - expect(execa.mock.results[0].value.params).toEqual( - expect.arrayContaining(['--bazinga', false]), - ) - - expect(execa.mock.results[0].value.params).toEqual( - expect.arrayContaining(['--hello', 'world']), - ) -}) diff --git a/packages/cli/src/commands/__tests__/test.test.ts b/packages/cli/src/commands/__tests__/test.test.ts new file mode 100644 index 0000000000..9d1d74ff59 --- /dev/null +++ b/packages/cli/src/commands/__tests__/test.test.ts @@ -0,0 +1,144 @@ +import fs from 'node:fs' + +import execa from 'execa' +import { vi, afterEach, test, expect, beforeEach } from 'vitest' + +globalThis.__dirname = import.meta.dirname +import '../../lib/test.js' + +vi.mock('execa', () => ({ + default: vi.fn((cmd, params) => ({ + cmd, + params, + })), +})) + +import { handler } from '../test.js' + +vi.mock('@cedarjs/structure', () => { + return { + getProject: () => ({ + sides: ['web', 'api'], + }), + } +}) + +beforeEach(() => { + vi.spyOn(fs, 'existsSync').mockReturnValue(true) +}) + +afterEach(() => { + vi.clearAllMocks() +}) + +const defaultOptions = { + filter: [], + watch: false, + collectCoverage: false, + dbPush: false, +} + +test('Runs tests for all available sides if no filter passed', async () => { + await handler(defaultOptions) + + expect(vi.mocked(execa).mock.results[0].value.cmd).toBe('yarn') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('jest') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('web') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('api') +}) + +test('Syncs or creates test database when the flag --db-push is set to true', async () => { + await handler({ ...defaultOptions, filter: ['api'], dbPush: true }) + + expect(vi.mocked(execa).mock.results[0].value.cmd).toBe('yarn') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('jest') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('--projects') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('api') +}) + +test('Skips test database sync/creation when the flag --db-push is set to false', async () => { + await handler({ ...defaultOptions, filter: ['api'], dbPush: false }) + + expect(vi.mocked(execa).mock.results[0].value.cmd).toBe('yarn') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('jest') +}) + +test('Runs tests for all available sides if no side filter passed', async () => { + await handler({ + ...defaultOptions, + filter: ['bazinga'], + }) + + expect(vi.mocked(execa).mock.results[0].value.cmd).toBe('yarn') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('jest') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('bazinga') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('web') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('api') +}) + +test('Runs tests specified side if even with additional filters', async () => { + await handler({ ...defaultOptions, filter: ['web', 'bazinga'] }) + + expect(vi.mocked(execa).mock.results[0].value.cmd).not.toBe('yarn rw') + expect(vi.mocked(execa).mock.results[0].value.params).not.toContain('api') + + expect(vi.mocked(execa).mock.results[0].value.cmd).toBe('yarn') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('jest') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('bazinga') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('web') +}) + +test('Does not create db when calling test with just web', async () => { + await handler({ + ...defaultOptions, + filter: ['web'], + }) + + expect(vi.mocked(execa).mock.results[0].value.cmd).toBe('yarn') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('jest') +}) + +test('Passes filter param to jest command if passed', async () => { + await handler({ ...defaultOptions, filter: ['web', 'bazinga'] }) + + expect(vi.mocked(execa).mock.results[0].value.cmd).toBe('yarn') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('jest') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('bazinga') +}) + +test('Passes other flags to jest', async () => { + await handler({ + ...defaultOptions, + u: true, + debug: true, + json: true, + collectCoverage: true, + }) + + expect(vi.mocked(execa).mock.results[0].value.cmd).toBe('yarn') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('jest') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('-u') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('--debug') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('--json') + expect(vi.mocked(execa).mock.results[0].value.params).toContain( + '--collectCoverage', + ) +}) + +test('Passes values of other flags to jest', async () => { + await handler({ ...defaultOptions, bazinga: false, hello: 'world' }) + + // Second command because api side runs + expect(vi.mocked(execa).mock.results[0].value.cmd).toBe('yarn') + expect(vi.mocked(execa).mock.results[0].value.params).toContain('jest') + + // Note that these below tests aren't the best, since they don't check for order + // But I'm making sure only 2 extra params get passed + expect(vi.mocked(execa).mock.results[0].value.params).toEqual( + expect.arrayContaining(['--bazinga', false]), + ) + + expect(vi.mocked(execa).mock.results[0].value.params).toEqual( + expect.arrayContaining(['--hello', 'world']), + ) +}) diff --git a/packages/cli/src/commands/__tests__/type-check.test.js b/packages/cli/src/commands/__tests__/type-check.test.ts similarity index 70% rename from packages/cli/src/commands/__tests__/type-check.test.js rename to packages/cli/src/commands/__tests__/type-check.test.ts index fd22f8f12a..e0027a912a 100644 --- a/packages/cli/src/commands/__tests__/type-check.test.js +++ b/packages/cli/src/commands/__tests__/type-check.test.ts @@ -1,3 +1,11 @@ +import path from 'node:path' + +import concurrently from 'concurrently' +import execa from 'execa' +import { vi, beforeEach, afterEach, test, expect } from 'vitest' + +import '../../lib/mockTelemetry.js' + vi.mock('execa', () => ({ default: vi.fn((cmd, params, options) => { return { @@ -15,20 +23,21 @@ vi.mock('concurrently', () => ({ })), })) -import '../../lib/mockTelemetry' - -let mockedRedwoodConfig = { +const mockedRedwoodConfig = { api: {}, web: {}, browser: {}, } vi.mock('../../lib', async (importOriginal) => { - const originalLib = await importOriginal() + const originalLib = await importOriginal() return { ...originalLib, runCommandTask: vi.fn((commands) => { - return commands.map(({ cmd, args }) => `${cmd} ${args?.join(' ')}`) + return commands.map( + ({ cmd, args }: { cmd: string; args?: string[] }) => + `${cmd} ${args?.join(' ')}`, + ) }), getPaths: () => ({ base: './myBasePath', @@ -43,12 +52,9 @@ vi.mock('../../lib', async (importOriginal) => { } }) -import path from 'path' - -import concurrently from 'concurrently' -import execa from 'execa' -import { vi, beforeEach, afterEach, test, expect } from 'vitest' - +// @ts-expect-error - No types for .js files +import type * as Lib from '../../lib/index.js' +// @ts-expect-error - No types for .js files import { runCommandTask } from '../../lib/index.js' import { handler } from '../type-check.js' @@ -59,8 +65,8 @@ beforeEach(() => { afterEach(() => { vi.clearAllMocks() - console.info.mockRestore() - console.log.mockRestore() + vi.mocked(console).info.mockRestore() + vi.mocked(console).log.mockRestore() }) test('Should run tsc commands correctly, in order', async () => { @@ -68,11 +74,12 @@ test('Should run tsc commands correctly, in order', async () => { sides: ['web', 'api'], prisma: false, generate: true, + verbose: false, }) - const concurrentlyArgs = concurrently.mock.results[0].value + const concurrentlyArgs = vi.mocked(concurrently).mock.results[0].value - expect(execa.mock.results[0].value.cmd).toEqual('yarn rw-gen') + expect(vi.mocked(execa).mock.results[0].value.cmd).toEqual('yarn rw-gen') // Ensure tsc command run correctly for web side expect(concurrentlyArgs.commands).toContainEqual({ @@ -93,11 +100,12 @@ test('Should generate prisma client', async () => { sides: ['api'], prisma: true, generate: true, + verbose: false, }) - const concurrentlyArgs = concurrently.mock.results[0].value + const concurrentlyArgs = vi.mocked(concurrently).mock.results[0].value - expect(execa.mock.results[0].value.cmd).toEqual('yarn rw-gen') + expect(vi.mocked(execa).mock.results[0].value.cmd).toEqual('yarn rw-gen') // Ensure tsc command run correctly for web side expect(concurrentlyArgs.commands).toContainEqual({ @@ -105,7 +113,7 @@ test('Should generate prisma client', async () => { command: 'yarn tsc --noEmit --skipLibCheck', }) - expect(runCommandTask.mock.results[0].value[0]).toMatch( + expect(vi.mocked(runCommandTask).mock.results[0].value[0]).toMatch( /.+(\\|\/)prisma(\\|\/)build(\\|\/)index.js.+/, ) }) diff --git a/packages/cli/src/commands/__tests__/upgrade.test.ts b/packages/cli/src/commands/__tests__/upgrade.test.ts index 75f0c1b2a9..d9e93a43cc 100644 --- a/packages/cli/src/commands/__tests__/upgrade.test.ts +++ b/packages/cli/src/commands/__tests__/upgrade.test.ts @@ -1,10 +1,8 @@ import fs from 'node:fs' -import type execa from 'execa' import { dedent } from 'ts-dedent' import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest' -// @ts-expect-error - JS file import { runPreUpgradeScripts } from '../upgrade.js' // Mock fetch globally @@ -39,8 +37,8 @@ vi.mock('node:os', () => ({ })) describe('runPreUpgradeScripts', () => { - let mockTask: any - let mockCtx: any + let mockTask: { output: string } + let mockCtx: Record beforeEach(() => { mockTask = { @@ -140,8 +138,7 @@ describe('runPreUpgradeScripts', () => { console.log('Running upgrade check') ` - // Mock readFile to return the script content - vi.mocked(fs.promises.readFile).mockResolvedValue(scriptContent as any) + vi.mocked(fs.promises.readFile).mockResolvedValue(scriptContent) vi.mocked(fetch).mockImplementation(async (url: string | URL | Request) => { if (url.toString().endsWith('/manifest.json')) { @@ -164,31 +161,35 @@ describe('runPreUpgradeScripts', () => { throw new Error(`Unexpected url: ${url}`) }) - vi.mocked(execa.default).mockImplementation((( - command: string, - argsOrOptions: string[] | unknown, - _maybeOptions: unknown, - ) => { - // Handle overloaded signature where second param could be options - const actualArgs = Array.isArray(argsOrOptions) ? argsOrOptions : [] - - if (command === 'npm' && actualArgs?.includes('install')) { - return { - stdout: '', - stderr: '', - } as unknown as execa.ExecaChildProcess - } else if (command === 'node') { - return { - stdout: 'Upgrade check passed', - stderr: '', - } as unknown as execa.ExecaChildProcess - } - - throw new Error(`Unexpected command: ${command} ${actualArgs?.join(' ')}`) + vi.mocked(execa.default).mockImplementation( // TypeScript is struggling with the type for the function overload and - // for a test it's not worth it to mock this properly. That's why we use - // `as any` here. - }) as any) + // for a test it's not worth it to mock this properly. + // @ts-expect-error - Only mocking the implementation we're using + ( + command: string, + argsOrOptions: string[] | unknown, + _maybeOptions: unknown, + ) => { + // Handle overloaded signature where second param could be options + const actualArgs = Array.isArray(argsOrOptions) ? argsOrOptions : [] + + if (command === 'npm' && actualArgs?.includes('install')) { + return { + stdout: '', + stderr: '', + } + } else if (command === 'node') { + return { + stdout: 'Upgrade check passed', + stderr: '', + } + } + + throw new Error( + `Unexpected command: ${command} ${actualArgs?.join(' ')}`, + ) + }, + ) await runPreUpgradeScripts(mockCtx, mockTask, { verbose: false, @@ -225,7 +226,7 @@ describe('runPreUpgradeScripts', () => { // Verify script was executed expect(execa.default).toHaveBeenCalledWith( 'node', - ['script.mts', '--verbose', false, '--force', false], + ['script.mts', '--verbose', 'false', '--force', 'false'], { cwd: '/tmp/cedar-upgrade-abc123', }, diff --git a/packages/cli/src/commands/build.js b/packages/cli/src/commands/build.ts similarity index 77% rename from packages/cli/src/commands/build.js rename to packages/cli/src/commands/build.ts index bc7ead305e..1045fde485 100644 --- a/packages/cli/src/commands/build.js +++ b/packages/cli/src/commands/build.ts @@ -1,14 +1,19 @@ import { terminalLink } from 'termi-link' +import type { Argv } from 'yargs' +// @ts-expect-error - Types not available for JS files import c from '../lib/colors.js' +// @ts-expect-error - Types not available for JS files import { exitWithError } from '../lib/exit.js' +// @ts-expect-error - Types not available for JS files import { sides } from '../lib/project.js' +// @ts-expect-error - Types not available for JS files import { checkNodeVersion } from '../middleware/checkNodeVersion.js' export const command = 'build [side..]' export const description = 'Build for production' -export const builder = (yargs) => { +export const builder = (yargs: Argv) => { const choices = sides() yargs @@ -16,7 +21,8 @@ export const builder = (yargs) => { choices, default: choices, description: 'Which side(s) to build', - type: 'array', + type: 'string', + array: true, }) .option('verbose', { alias: 'v', @@ -55,7 +61,7 @@ export const builder = (yargs) => { ) } -export const handler = async (options) => { +export const handler = async (options: Record) => { const { handler } = await import('./buildHandler.js') return handler(options) } diff --git a/packages/cli/src/commands/buildHandler.js b/packages/cli/src/commands/buildHandler.ts similarity index 93% rename from packages/cli/src/commands/buildHandler.js rename to packages/cli/src/commands/buildHandler.ts index 6556870f09..b581de3b86 100644 --- a/packages/cli/src/commands/buildHandler.js +++ b/packages/cli/src/commands/buildHandler.ts @@ -13,21 +13,30 @@ import { loadAndValidateSdls } from '@cedarjs/internal/dist/validateSchema' import { detectPrerenderRoutes } from '@cedarjs/prerender/detection' import { timedTelemetry } from '@cedarjs/telemetry' +// @ts-expect-error - Types not available for JS files import { generatePrismaCommand } from '../lib/generatePrismaClient.js' +// @ts-expect-error - Types not available for JS files import { getPaths, getConfig } from '../lib/index.js' +interface BuildOptions { + side?: string[] + verbose?: boolean + prisma?: boolean + prerender?: boolean +} + export const handler = async ({ side = ['api', 'web'], verbose = false, prisma = true, prerender, -}) => { +}: BuildOptions) => { recordTelemetryAttributes({ command: 'build', side: JSON.stringify(side), verbose, prisma, - prerender, + prerender: !!prerender, }) const rwjsPaths = getPaths() @@ -133,7 +142,9 @@ export const handler = async ({ } }, }, - ].filter(Boolean) + ].filter( + (x): x is Exclude => !!x, + ) const triggerPrerender = async () => { console.log('Starting prerendering...') @@ -158,6 +169,7 @@ export const handler = async ({ } const jobs = new Listr(tasks, { + // @ts-expect-error - renderer might be incompatible renderer: verbose && 'verbose', }) diff --git a/packages/cli/src/commands/check.js b/packages/cli/src/commands/check.ts similarity index 84% rename from packages/cli/src/commands/check.js rename to packages/cli/src/commands/check.ts index 455ddde4bb..199db7503c 100644 --- a/packages/cli/src/commands/check.js +++ b/packages/cli/src/commands/check.ts @@ -1,6 +1,8 @@ import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' +// @ts-expect-error - Types not available for JS files import c from '../lib/colors.js' +// @ts-expect-error - Types not available for JS files import { getPaths } from '../lib/index.js' export const command = 'check' @@ -18,7 +20,7 @@ export const handler = async () => { console.log('DiagnosticServerity', DiagnosticSeverity) printDiagnostics(getPaths().base, { - getSeverityLabel: (severity) => { + getSeverityLabel: (severity: unknown) => { if (severity === DiagnosticSeverity.Error) { return c.error('error') } diff --git a/packages/cli/src/commands/console.js b/packages/cli/src/commands/console.ts similarity index 69% rename from packages/cli/src/commands/console.js rename to packages/cli/src/commands/console.ts index 0d8b66c4f9..ed1a770254 100644 --- a/packages/cli/src/commands/console.js +++ b/packages/cli/src/commands/console.ts @@ -2,7 +2,7 @@ export const command = 'console' export const aliases = ['c'] export const description = 'Launch an interactive Redwood shell (experimental)' -export const handler = async (options) => { +export const handler = async (_options: Record) => { const { handler } = await import('./consoleHandler.js') - return handler(options) + return handler() } diff --git a/packages/cli/src/commands/consoleHandler.js b/packages/cli/src/commands/consoleHandler.ts similarity index 64% rename from packages/cli/src/commands/consoleHandler.js rename to packages/cli/src/commands/consoleHandler.ts index 59944b82eb..89021fc4b3 100644 --- a/packages/cli/src/commands/consoleHandler.js +++ b/packages/cli/src/commands/consoleHandler.ts @@ -6,11 +6,12 @@ import repl from 'node:repl' import { registerApiSideBabelHook } from '@cedarjs/babel-config' import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' +// @ts-expect-error - Types not available for JS files import { getPaths } from '../lib/index.js' const paths = getPaths() -const loadPrismaClient = (replContext) => { +const loadPrismaClient = (replContext: Record) => { const createdRequire = createRequire(import.meta.url) const { db } = createdRequire(path.join(paths.api.lib, 'db')) // workaround for Prisma issue: https://github.com/prisma/prisma/issues/18292 @@ -19,22 +20,37 @@ const loadPrismaClient = (replContext) => { } const consoleHistoryFile = path.join(paths.generated.base, 'console_history') -const persistConsoleHistory = (r) => { + +interface REPLServerWithHistory extends repl.REPLServer { + history: string[] + lines: string[] +} + +function isREPLServerWithHistory( + replServer: repl.REPLServer, +): replServer is REPLServerWithHistory { + return 'history' in replServer && 'lines' in replServer +} + +const persistConsoleHistory = (r: repl.REPLServer) => { + const lines = isREPLServerWithHistory(r) ? r.lines : [] fs.appendFileSync( consoleHistoryFile, - r.lines.filter((line) => line.trim()).join('\n') + '\n', + lines.filter((line: string) => line.trim()).join('\n') + '\n', 'utf8', ) } -const loadConsoleHistory = async (r) => { +const loadConsoleHistory = async (r: repl.REPLServer) => { try { const history = await fs.promises.readFile(consoleHistoryFile, 'utf8') - history - .split('\n') - .reverse() - .map((line) => r.history.push(line)) - } catch (e) { + if (isREPLServerWithHistory(r)) { + history + .split('\n') + .reverse() + .map((line) => r.history.push(line)) + } + } catch { // We can ignore this -- it just means the user doesn't have any history yet } } @@ -64,17 +80,18 @@ export const handler = () => { // always await promises. // source: https://github.com/nodejs/node/issues/13209#issuecomment-619526317 const defaultEval = r.eval - r.eval = (cmd, context, filename, callback) => { - defaultEval(cmd, context, filename, async (err, result) => { + // @ts-expect-error - overriding eval signature + r.eval = function (this: repl.REPLServer, cmd, context, filename, callback) { + defaultEval.call(this, cmd, context, filename, async (err, result) => { if (err) { // propagate errors. - callback(err) + callback(err, null) } else { // await the promise and either return the result or error. try { callback(null, await Promise.resolve(result)) - } catch (err) { - callback(err) + } catch (err: unknown) { + callback(err instanceof Error ? err : new Error(String(err)), null) } } }) diff --git a/packages/cli/src/commands/destroy.js b/packages/cli/src/commands/destroy.ts similarity index 71% rename from packages/cli/src/commands/destroy.js rename to packages/cli/src/commands/destroy.ts index a495b217a6..9ee0ce1631 100644 --- a/packages/cli/src/commands/destroy.js +++ b/packages/cli/src/commands/destroy.ts @@ -1,19 +1,30 @@ import { terminalLink } from 'termi-link' +import type { Argv } from 'yargs' +// @ts-expect-error - No types for .js files import * as destroyCell from './destroy/cell/cell.js' +// @ts-expect-error - No types for .js files import * as destroyComponent from './destroy/component/component.js' +// @ts-expect-error - No types for .js files import * as destroyDirective from './destroy/directive/directive.js' +// @ts-expect-error - No types for .js files import * as destroyFunction from './destroy/function/function.js' +// @ts-expect-error - No types for .js files import * as destroyLayout from './destroy/layout/layout.js' +// @ts-expect-error - No types for .js files import * as destroyPage from './destroy/page/page.js' +// @ts-expect-error - No types for .js files import * as destroyScaffold from './destroy/scaffold/scaffold.js' +// @ts-expect-error - No types for .js files import * as destroySdl from './destroy/sdl/sdl.js' +// @ts-expect-error - No types for .js files import * as destroyService from './destroy/service/service.js' export const command = 'destroy ' export const aliases = ['d'] export const description = 'Rollback changes made by the generate command' -export const builder = (yargs) => + +export const builder = (yargs: Argv) => yargs .command(destroyCell) .command(destroyComponent) diff --git a/packages/cli/src/commands/dev.ts b/packages/cli/src/commands/dev.ts index d42f35a019..7eca0d40ac 100644 --- a/packages/cli/src/commands/dev.ts +++ b/packages/cli/src/commands/dev.ts @@ -55,7 +55,7 @@ export const builder = (yargs: Argv) => { ) } -export const handler = async (options: any) => { +export const handler = async (options: Record) => { const { handler } = await import('./devHandler.js') return handler(options) } diff --git a/packages/cli/src/commands/devHandler.ts b/packages/cli/src/commands/devHandler.ts index dc221ab8d3..bc7e49552c 100644 --- a/packages/cli/src/commands/devHandler.ts +++ b/packages/cli/src/commands/devHandler.ts @@ -121,9 +121,8 @@ export const handler = async ({ verbose: false, force: false, }) - } catch (e) { - const message = - e instanceof Object && 'message' in e ? e.message : String(e) + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) errorTelemetry(process.argv, `Error generating prisma client: ${message}`) console.error(c.error(message)) } @@ -133,9 +132,8 @@ export const handler = async ({ if (!serverFile) { try { await shutdownPort(apiAvailablePort) - } catch (e) { - const message = - e instanceof Object && 'message' in e ? e.message : String(e) + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) errorTelemetry(process.argv, `Error shutting down "api": ${message}`) console.error( `Error whilst shutting down "api" port: ${c.error(message)}`, @@ -147,9 +145,8 @@ export const handler = async ({ if (side.includes('web')) { try { await shutdownPort(webAvailablePort) - } catch (e) { - const message = - e instanceof Object && 'message' in e ? e.message : String(e) + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) errorTelemetry(process.argv, `Error shutting down "web": ${message}`) console.error( `Error whilst shutting down "web" port: ${c.error(message)}`, @@ -274,6 +271,7 @@ export const handler = async ({ process.argv, `Error concurrently starting sides: ${e.message}`, ) + exitWithError(e) } }) diff --git a/packages/cli/src/commands/exec.js b/packages/cli/src/commands/exec.ts similarity index 87% rename from packages/cli/src/commands/exec.js rename to packages/cli/src/commands/exec.ts index c31de27d2a..3474ea3df0 100644 --- a/packages/cli/src/commands/exec.js +++ b/packages/cli/src/commands/exec.ts @@ -1,8 +1,9 @@ import { terminalLink } from 'termi-link' +import type { Argv } from 'yargs' export const command = 'exec [name]' export const description = 'Run scripts generated with yarn generate script' -export const builder = (yargs) => { +export const builder = (yargs: Argv) => { yargs .positional('name', { description: 'The file name (extension is optional) of the script to run', @@ -34,7 +35,7 @@ export const builder = (yargs) => { ) } -export const handler = async (options) => { +export const handler = async (options: Record) => { const { handler } = await import('./execHandler.js') return handler(options) } diff --git a/packages/cli/src/commands/execHandler.js b/packages/cli/src/commands/execHandler.ts similarity index 77% rename from packages/cli/src/commands/execHandler.js rename to packages/cli/src/commands/execHandler.ts index d3100bf12c..2b6f87ae44 100644 --- a/packages/cli/src/commands/execHandler.js +++ b/packages/cli/src/commands/execHandler.ts @@ -8,24 +8,31 @@ import { Listr } from 'listr2' import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' import { findScripts } from '@cedarjs/internal/dist/files' +// @ts-expect-error - Types not available for JS files import c from '../lib/colors.js' +// @ts-expect-error - Types not available for JS files import { runScriptFunction } from '../lib/exec.js' +// @ts-expect-error - Types not available for JS files import { generatePrismaClient } from '../lib/generatePrismaClient.js' +// @ts-expect-error - Types not available for JS files import { getPaths } from '../lib/index.js' const printAvailableScriptsToConsole = () => { // Loop through all scripts and get their relative path // Also group scripts with the same name but different extensions - const scripts = findScripts(getPaths().scripts).reduce((acc, scriptPath) => { - const relativePath = path.relative(getPaths().scripts, scriptPath) - const ext = path.parse(relativePath).ext - const pathNoExt = relativePath.slice(0, -ext.length) + const scripts = findScripts(getPaths().scripts).reduce( + (acc: Record, scriptPath: string) => { + const relativePath = path.relative(getPaths().scripts, scriptPath) + const ext = path.parse(relativePath).ext + const pathNoExt = relativePath.slice(0, -ext.length) - acc[pathNoExt] ||= [] - acc[pathNoExt].push(relativePath) + acc[pathNoExt] ||= [] + acc[pathNoExt].push(relativePath) - return acc - }, {}) + return acc + }, + {}, + ) console.log('Available scripts:') Object.entries(scripts).forEach(([name, paths]) => { @@ -42,11 +49,19 @@ const printAvailableScriptsToConsole = () => { console.log() } -export const handler = async (args) => { +interface ExecOptions { + name?: string + prisma?: boolean + list?: boolean + silent?: boolean + [key: string]: unknown +} + +export const handler = async (args: ExecOptions) => { recordTelemetryAttributes({ command: 'exec', - prisma: args.prisma, - list: args.list, + prisma: !!args.prisma, + list: !!args.list, }) const { name, prisma, list, ...scriptArgs } = args @@ -70,7 +85,9 @@ export const handler = async (args) => { // arguments we haven't given a name. // `'exec'` is of no interest to the user, as its not meant to be an argument // to their script. And so we remove it from the array. - scriptArgs._ = scriptArgs._.slice(1) + if (Array.isArray(scriptArgs._)) { + scriptArgs._ = scriptArgs._.slice(1) + } // 'rw' is not meant for the script's args, so delete that delete scriptArgs.$0 @@ -97,12 +114,12 @@ export const handler = async (args) => { const scriptTasks = [ { title: 'Generating Prisma client', - enabled: () => prisma, + enabled: () => !!prisma, task: () => generatePrismaClient({ force: false, verbose: !args.silent, - silent: args.silent, + silent: !!args.silent, }), }, { @@ -115,7 +132,8 @@ export const handler = async (args) => { args: { args: scriptArgs }, }) } catch (e) { - console.error(c.error(`Error in script: ${e.message}`)) + const message = e instanceof Error ? e.message : String(e) + console.error(c.error(`Error in script: ${message}`)) throw e } }, @@ -123,7 +141,6 @@ export const handler = async (args) => { ] const tasks = new Listr(scriptTasks, { - rendererOptions: { collapseSubtasks: false }, renderer: args.silent ? 'silent' : 'verbose', }) @@ -133,7 +150,7 @@ export const handler = async (args) => { }) } -function resolveScriptPath(name) { +function resolveScriptPath(name: string) { const scriptPath = path.join(getPaths().scripts, name) // If scriptPath already has an extension, and it's a valid path, return it @@ -144,7 +161,7 @@ function resolveScriptPath(name) { // These extensions match the ones in internal/src/files.ts::findScripts() const extensions = ['.js', '.jsx', '.ts', '.tsx'] - const matches = [] + const matches: string[] = [] for (const extension of extensions) { const p = scriptPath + extension @@ -159,7 +176,7 @@ function resolveScriptPath(name) { } else if (matches.length > 1) { console.error( c.error( - `\nMultiple scripts found for \`${name}\`. Please specify the ` + + `\nMultiple scripts found for \`\${name}\`. Please specify the ` + 'extension.', ), ) diff --git a/packages/cli/src/commands/experimental.js b/packages/cli/src/commands/experimental.ts similarity index 75% rename from packages/cli/src/commands/experimental.js rename to packages/cli/src/commands/experimental.ts index 117d53c872..367dbd3666 100644 --- a/packages/cli/src/commands/experimental.js +++ b/packages/cli/src/commands/experimental.ts @@ -1,18 +1,25 @@ import { terminalLink } from 'termi-link' +import type { Argv } from 'yargs' +// @ts-expect-error - No types for .js files import detectRxVersion from '../middleware/detectProjectRxVersion.js' +// @ts-expect-error - No types for .js files import * as experimentalInngest from './experimental/setupInngest.js' +// @ts-expect-error - No types for .js files import * as experimentalOpenTelemetry from './experimental/setupOpentelemetry.js' +// @ts-expect-error - No types for .js files import * as experimentalReactCompiler from './experimental/setupReactCompiler.js' +// @ts-expect-error - No types for .js files import * as experimentalRsc from './experimental/setupRsc.js' +// @ts-expect-error - No types for .js files import * as experimentalStreamingSsr from './experimental/setupStreamingSsr.js' export const command = 'experimental ' export const aliases = ['exp'] export const description = 'Run or setup experimental features' -export const builder = (yargs) => +export const builder = (yargs: Argv) => yargs .command(experimentalInngest) .command(experimentalOpenTelemetry) diff --git a/packages/cli/src/commands/generate.js b/packages/cli/src/commands/generate.ts similarity index 68% rename from packages/cli/src/commands/generate.js rename to packages/cli/src/commands/generate.ts index b63f21a271..73cdfc0d86 100644 --- a/packages/cli/src/commands/generate.js +++ b/packages/cli/src/commands/generate.ts @@ -1,41 +1,67 @@ import execa from 'execa' import { terminalLink } from 'termi-link' +import type { Argv } from 'yargs' import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' +// @ts-expect-error - No types for .js files import * as generateCell from './generate/cell/cell.js' +// @ts-expect-error - No types for .js files import * as generateComponent from './generate/component/component.js' +// @ts-expect-error - No types for .js files import * as generateDataMigration from './generate/dataMigration/dataMigration.js' +// @ts-expect-error - No types for .js files import * as generateDbAuth from './generate/dbAuth/dbAuth.js' +// @ts-expect-error - No types for .js files import * as generateDirective from './generate/directive/directive.js' +// @ts-expect-error - No types for .js files import * as generateFunction from './generate/function/function.js' +// @ts-expect-error - No types for .js files import * as generateJob from './generate/job/job.js' +// @ts-expect-error - No types for .js files import * as generateLayout from './generate/layout/layout.js' +// @ts-expect-error - No types for .js files import * as generateModel from './generate/model/model.js' +// @ts-expect-error - No types for .js files import * as generateOgImage from './generate/ogImage/ogImage.js' +// @ts-expect-error - No types for .js files import * as generatePage from './generate/page/page.js' +// @ts-expect-error - No types for .js files import * as generateRealtime from './generate/realtime/realtime.js' +// @ts-expect-error - No types for .js files import * as generateScaffold from './generate/scaffold/scaffold.js' +// @ts-expect-error - No types for .js files import * as generateScript from './generate/script/script.js' +// @ts-expect-error - No types for .js files import * as generateSdl from './generate/sdl/sdl.js' +// @ts-expect-error - No types for .js files import * as generateSecret from './generate/secret/secret.js' +// @ts-expect-error - No types for .js files import * as generateService from './generate/service/service.js' export const command = 'generate ' export const aliases = ['g'] export const description = 'Generate boilerplate code and type definitions' -export const builder = (yargs) => +export const builder = (yargs: Argv) => yargs .command('types', 'Generate supplementary code', {}, () => { recordTelemetryAttributes({ command: 'generate types' }) try { execa.sync('yarn', ['rw-gen'], { stdio: 'inherit' }) - } catch (error) { + } catch (error: unknown) { // rw-gen is responsible for logging its own errors but we need to // make sure we exit with a non-zero exit code - process.exitCode = error.exitCode ?? 1 + if ( + error instanceof Object && + 'exitCode' in error && + typeof error.exitCode === 'number' + ) { + process.exitCode = error.exitCode + } else { + process.exitCode = 1 + } } }) .command(generateCell) diff --git a/packages/cli/src/commands/info.js b/packages/cli/src/commands/info.ts similarity index 90% rename from packages/cli/src/commands/info.js rename to packages/cli/src/commands/info.ts index a39e03a788..71d50b6b6a 100644 --- a/packages/cli/src/commands/info.js +++ b/packages/cli/src/commands/info.ts @@ -4,13 +4,14 @@ import fs from 'node:fs' import envinfo from 'envinfo' import { terminalLink } from 'termi-link' +import type { Argv } from 'yargs' import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' import { getPaths } from '@cedarjs/project-config' export const command = 'info' export const description = 'Print your system environment information' -export const builder = (yargs) => { +export const builder = (yargs: Argv) => { yargs.epilogue( `Also see the ${terminalLink( 'CedarJS CLI Reference', @@ -38,7 +39,7 @@ export const handler = async () => { redwoodToml .split('\n') .filter((line) => line.trim().length > 0) - .filter((line) => !/^#/.test(line)) + .filter((line) => !line.startsWith('#')) .map((line) => ` ${line}`) .join('\n'), ) diff --git a/packages/cli/src/commands/lint.js b/packages/cli/src/commands/lint.ts similarity index 74% rename from packages/cli/src/commands/lint.js rename to packages/cli/src/commands/lint.ts index e12b428fdc..bbff17c215 100644 --- a/packages/cli/src/commands/lint.js +++ b/packages/cli/src/commands/lint.ts @@ -3,16 +3,16 @@ import path from 'node:path' import execa from 'execa' import { terminalLink } from 'termi-link' +import type { Argv } from 'yargs' import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' - -import { getPaths, getConfig } from '../lib/index.js' +import { getPaths, getConfig } from '@cedarjs/project-config' /** * Checks for legacy ESLint configuration files in the project root - * @returns {string[]} Array of legacy config file names found + * @returns Array of legacy config file names found */ -function detectLegacyEslintConfig() { +function detectLegacyEslintConfig(): string[] { const projectRoot = getPaths().base const legacyConfigFiles = [ '.eslintrc.js', @@ -22,7 +22,7 @@ function detectLegacyEslintConfig() { '.eslintrc.yml', ] - const foundLegacyFiles = [] + const foundLegacyFiles: string[] = [] // Check for .eslintrc.* files for (const configFile of legacyConfigFiles) { @@ -42,7 +42,7 @@ function detectLegacyEslintConfig() { if (packageJson.eslint) { foundLegacyFiles.push('package.json (eslint field)') } - } catch (error) { + } catch { // Ignore JSON parse errors } } @@ -52,9 +52,9 @@ function detectLegacyEslintConfig() { /** * Shows a deprecation warning for legacy ESLint configuration - * @param {string[]} legacyFiles Array of legacy config file names + * @param legacyFiles Array of legacy config file names */ -function showLegacyEslintDeprecationWarning(legacyFiles) { +function showLegacyEslintDeprecationWarning(legacyFiles: string[]) { console.warn('') console.warn('⚠️ DEPRECATION WARNING: Legacy ESLint Configuration Detected') console.warn('') @@ -81,12 +81,13 @@ function showLegacyEslintDeprecationWarning(legacyFiles) { export const command = 'lint [path..]' export const description = 'Lint your files' -export const builder = (yargs) => { +export const builder = (yargs: Argv) => { yargs .positional('path', { description: 'Specify file(s) or directory(ies) to lint relative to project root', - type: 'array', + type: 'string', + array: true, }) .option('fix', { default: false, @@ -106,18 +107,32 @@ export const builder = (yargs) => { ) } -export const handler = async ({ path, fix, format }) => { - recordTelemetryAttributes({ command: 'lint', fix, format }) +interface LintOptions { + path?: string[] + fix?: boolean + format?: string +} + +export const handler = async ({ path: filePath, fix, format }: LintOptions) => { + recordTelemetryAttributes({ + command: 'lint', + fix: !!fix, + format: format ?? 'stylish', + }) - // Check for legacy ESLint configuration and show deprecation warning const config = getConfig() const legacyConfigFiles = detectLegacyEslintConfig() - if (legacyConfigFiles.length > 0 && config.eslintLegacyConfigWarning) { + if ( + legacyConfigFiles.length > 0 && + config instanceof Object && + 'eslintLegacyConfigWarning' in config && + config.eslintLegacyConfigWarning + ) { showLegacyEslintDeprecationWarning(legacyConfigFiles) } try { - const pathString = path?.join(' ') + const pathString = filePath?.join(' ') const sbPath = getPaths().web.storybook const args = [ 'eslint', @@ -130,7 +145,7 @@ export const handler = async ({ path, fix, format }) => { !pathString && fs.existsSync(getPaths().scripts) && 'scripts', !pathString && fs.existsSync(getPaths().api.src) && 'api/src', pathString, - ].filter(Boolean) + ].filter((x): x is string => !!x) const result = await execa('yarn', args, { cwd: getPaths().base, @@ -138,7 +153,15 @@ export const handler = async ({ path, fix, format }) => { }) process.exitCode = result.exitCode - } catch (error) { - process.exitCode = error.exitCode ?? 1 + } catch (error: unknown) { + if ( + error instanceof Object && + 'exitCode' in error && + typeof error.exitCode === 'number' + ) { + process.exitCode = error.exitCode + } else { + process.exitCode = 1 + } } } diff --git a/packages/cli/src/commands/prerender.js b/packages/cli/src/commands/prerender.ts similarity index 75% rename from packages/cli/src/commands/prerender.js rename to packages/cli/src/commands/prerender.ts index 259803f579..4a6b9ad76d 100644 --- a/packages/cli/src/commands/prerender.js +++ b/packages/cli/src/commands/prerender.ts @@ -1,8 +1,10 @@ +import type { Argv } from 'yargs' + export const command = 'prerender' export const aliases = ['render'] export const description = 'Prerender pages of your Redwood app at build time' -export const builder = (yargs) => { +export const builder = (yargs: Argv) => { yargs.showHelpOnFail(false) yargs.option('path', { @@ -26,7 +28,13 @@ export const builder = (yargs) => { }) } -export const handler = async (options) => { +interface PrerenderOptions { + path?: string + dryRun?: boolean + verbose?: boolean +} + +export const handler = async (options: PrerenderOptions) => { const { handler } = await import('./prerenderHandler.js') return handler(options) } diff --git a/packages/cli/src/commands/prerenderHandler.js b/packages/cli/src/commands/prerenderHandler.ts similarity index 75% rename from packages/cli/src/commands/prerenderHandler.js rename to packages/cli/src/commands/prerenderHandler.ts index 8101c89abb..56fad9b3fb 100644 --- a/packages/cli/src/commands/prerenderHandler.js +++ b/packages/cli/src/commands/prerenderHandler.ts @@ -4,16 +4,21 @@ import path from 'path' import { Listr } from 'listr2' import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' +import type * as Prerender from '@cedarjs/prerender' import { getConfig, getPaths, projectIsEsm } from '@cedarjs/project-config' import { errorTelemetry } from '@cedarjs/telemetry' +import type { QueryInfo } from '@cedarjs/web' +// @ts-expect-error - Types not available for JS files import c from '../lib/colors.js' +// @ts-expect-error - Types not available for JS files import { runScriptFunction } from '../lib/exec.js' +// @ts-expect-error - Types not available for JS files import { configureBabel } from '../lib/execBabel.js' class PathParamError extends Error {} -const mapRouterPathToHtml = (routerPath) => { +const mapRouterPathToHtml = (routerPath: string) => { if (routerPath === '/') { return 'web/dist/index.html' } else { @@ -21,7 +26,7 @@ const mapRouterPathToHtml = (routerPath) => { } } -function getRouteHooksFilePath(routeFilePath) { +function getRouteHooksFilePath(routeFilePath: string) { const routeHooksFilePathTs = routeFilePath.replace( /\.[jt]sx?$/, '.routeHooks.ts', @@ -43,6 +48,17 @@ function getRouteHooksFilePath(routeFilePath) { return undefined } +interface PrerenderRoute { + name: string + path: string + routePath: string + hasParams: boolean + id: string + isNotFound: boolean + filePath: string + pageIdentifier: string | undefined +} + /** * Takes a route with a path like /blog-post/{id:Int} * Reads path parameters from BlogPostPage.routeHooks.js and returns a list of @@ -74,7 +90,7 @@ function getRouteHooksFilePath(routeFilePath) { * When returning from this function, `path` in the above example will have * been replaced by an actual url, like /blog-post/15 */ -async function expandRouteParameters(route) { +async function expandRouteParameters(route: PrerenderRoute) { const routeHooksFilePath = getRouteHooksFilePath(route.filePath) if (!routeHooksFilePath) { @@ -82,7 +98,7 @@ async function expandRouteParameters(route) { } try { - const routeParameters = await runScriptFunction({ + const routeParameters: Record[] = await runScriptFunction({ path: routeHooksFilePath, functionName: 'routeParameters', args: { @@ -100,7 +116,7 @@ async function expandRouteParameters(route) { Object.entries(pathParamValues).forEach(([paramName, paramValue]) => { newPath = newPath.replace( new RegExp(`{${paramName}:?[^}]*}`), - paramValue, + String(paramValue), ) }) @@ -108,7 +124,8 @@ async function expandRouteParameters(route) { }) } } catch (e) { - console.error(c.error(e.stack)) + const stack = e instanceof Error ? e.stack : String(e) + console.error(c.error(stack)) return [route] } @@ -116,15 +133,27 @@ async function expandRouteParameters(route) { } // This is used directly in build.js for nested ListrTasks -export const getTasks = async (dryrun, routerPathFilter = null) => { +export const getTasks = async ( + dryrun: boolean, + routerPathFilter: string | null = null, +) => { const detector = projectIsEsm() ? await import('@cedarjs/prerender/detection') : await import('@cedarjs/prerender/cjs/detection') - const prerenderRoutes = detector - .detectPrerenderRoutes() - .filter((route) => route.path) - const indexHtmlPath = path.join(getPaths().web.dist, 'index.html') + const detectedRoutes: Partial[] = + detector.detectPrerenderRoutes() + + const prerenderRoutes = detectedRoutes.filter( + (route): route is PrerenderRoute => { + return ( + !!route.path && !!route.name && !!route.routePath && !!route.filePath + ) + }, + ) + + // TODO: Figure out how we want to handle the case where the user has only + // marked the NotFound route for prerender if (prerenderRoutes.length === 0) { console.log('\nSkipping prerender...') console.log( @@ -137,12 +166,14 @@ export const getTasks = async (dryrun, routerPathFilter = null) => { return [] } + // TODO: This should come before we even bother detecting routes to prerender + const indexHtmlPath = path.join(getPaths().web.dist, 'index.html') + if (!fs.existsSync(indexHtmlPath)) { console.error( 'You must run `yarn cedar build web` before trying to prerender.', ) process.exit(1) - // TODO: Run this automatically at this point. } configureBabel() @@ -177,14 +208,15 @@ export const getTasks = async (dryrun, routerPathFilter = null) => { 1, Math.floor(routesToPrerender.length / 100), ) - const title = (i) => + const title = (i: number) => `Prerendering ${routesToPrerender[0].name} (${i.toLocaleString()} of ${routesToPrerender.length.toLocaleString()})` return [ { title: title(0), - task: async (_, task) => { - // Note: This is a sequential loop, not parallelized as there have been previous issues - // with parallel prerendering. See:https://github.com/redwoodjs/redwood/pull/7321 + task: async (_: unknown, task: { title: string }) => { + // Note: This is a sequential loop, not parallelized as there have + // been previous issues with parallel prerendering. + // See: https://github.com/redwoodjs/redwood/pull/7321 for (let i = 0; i < routesToPrerender.length; i++) { const routeToPrerender = routesToPrerender[i] @@ -219,7 +251,12 @@ export const getTasks = async (dryrun, routerPathFilter = null) => { return routesToPrerender.map((routeToPrerender) => { // Filter out routes that don't match the supplied routePathFilter if (routerPathFilter && routeToPrerender.path !== routerPathFilter) { - return [] + // TODO: Figure out if it's an error to return [] here + // TS is complaining. If it is actually correct, we can use flatMap(), + // or append .flat() to the end of the map call that begins above + // This code was originally added in + // https://github.com/redwoodjs/graphql/pull/10888 + return [] as unknown as { title: string; task: () => Promise } } const outputHtmlPath = mapRouterPathToHtml(routeToPrerender.path) @@ -270,7 +307,7 @@ const diagnosticCheck = () => { ] console.log('Running diagnostic checks') - if (checks.some((checks) => checks.failure)) { + if (checks.some((check) => check.failure)) { console.error(c.error('node_modules are being duplicated in `./web` \n')) console.log('⚠️ Issues found: ') console.log('-'.repeat(50)) @@ -303,14 +340,13 @@ const diagnosticCheck = () => { } const prerenderRoute = async ( - prerenderer, - queryCache, - routeToPrerender, - dryrun, - outputHtmlPath, + prerenderer: typeof Prerender, + queryCache: Record, + routeToPrerender: PrerenderRoute, + dryrun: boolean, + outputHtmlPath: string, ) => { - // Check if route param templates in e.g. /path/{param1} have been replaced - if (/\{.*}/.test(routeToPrerender.path)) { + if (/\\{.*}/.test(routeToPrerender.path)) { throw new PathParamError( `Could not retrieve route parameters for ${routeToPrerender.path}`, ) @@ -322,10 +358,12 @@ const prerenderRoute = async ( renderPath: routeToPrerender.path, }) - if (!dryrun) { + if (!dryrun && typeof prerenderedHtml === 'string') { prerenderer.writePrerenderedHtmlFile(outputHtmlPath, prerenderedHtml) } - } catch (e) { + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) + const stack = e instanceof Error ? e.stack : String(e) console.log() console.log( c.warning('You can use `yarn cedar prerender --dry-run` to debug'), @@ -333,21 +371,29 @@ const prerenderRoute = async ( console.log() console.log( - `${c.info('-'.repeat(10))} Error rendering path "${ - routeToPrerender.path - }" ${c.info('-'.repeat(10))}`, + `${c.info('-'.repeat(10))} Error rendering path "${routeToPrerender.path}" ${c.info('-'.repeat(10))}`, ) - errorTelemetry(process.argv, `Error prerendering: ${e.message}`) + errorTelemetry(process.argv, `Error prerendering: ${message}`) - console.error(c.error(e.stack)) + console.error(c.error(stack)) console.log() throw new Error(`Failed to render "${routeToPrerender.filePath}"`) } } -export const handler = async ({ path: routerPath, dryRun, verbose }) => { +interface HandlerOptions { + path?: string + dryRun?: boolean + verbose?: boolean +} + +export const handler = async ({ + path: routerPath, + dryRun = false, + verbose = false, +}: HandlerOptions) => { if (getConfig().experimental?.streamingSsr?.enabled) { console.log( c.warning( @@ -364,23 +410,21 @@ export const handler = async ({ path: routerPath, dryRun, verbose }) => { verbose, }) - const listrTasks = await getTasks(dryRun, routerPath) - - const tasks = new Listr(listrTasks, { - renderer: verbose ? 'verbose' : 'default', - rendererOptions: { collapseSubtasks: false }, - concurrent: false, - }) + const listrTasks = await getTasks(dryRun, routerPath ?? null) try { if (dryRun) { console.log(c.info('::: Dry run, not writing changes :::')) } - await tasks.run() + await new Listr(listrTasks, { + renderer: verbose ? 'verbose' : 'default', + rendererOptions: { collapseSubtasks: false }, + concurrent: false, + }).run() } catch (e) { console.log() - await diagnosticCheck() + diagnosticCheck() console.log(c.warning('Tips:')) @@ -393,7 +437,7 @@ export const handler = async ({ path: routerPath, dryRun, verbose }) => { } else { console.log( c.info( - `- This could mean that a library you're using does not support SSR.`, + "- This could mean that a library you're using does not support SSR.", ), ) console.log( diff --git a/packages/cli/src/commands/prisma.js b/packages/cli/src/commands/prisma.ts similarity index 79% rename from packages/cli/src/commands/prisma.js rename to packages/cli/src/commands/prisma.ts index 062b501e05..8fcf242287 100644 --- a/packages/cli/src/commands/prisma.js +++ b/packages/cli/src/commands/prisma.ts @@ -1,10 +1,12 @@ +import type { Argv } from 'yargs' + export const command = 'prisma [commands..]' export const description = 'Run Prisma CLI with experimental features' /** * This is a lightweight wrapper around Prisma's CLI with some Cedar CLI modifications. */ -export const builder = (yargs) => { +export const builder = (yargs: Argv) => { // Disable yargs parsing of commands and options because it's forwarded // to Prisma CLI. yargs @@ -18,7 +20,7 @@ export const builder = (yargs) => { .version(false) } -export const handler = async (options) => { +export const handler = async (options: Record) => { const { handler } = await import('./prismaHandler.js') return handler(options) } diff --git a/packages/cli/src/commands/prismaHandler.js b/packages/cli/src/commands/prismaHandler.ts similarity index 73% rename from packages/cli/src/commands/prismaHandler.js rename to packages/cli/src/commands/prismaHandler.ts index 7f82234601..81bc1cba77 100644 --- a/packages/cli/src/commands/prismaHandler.js +++ b/packages/cli/src/commands/prismaHandler.ts @@ -1,5 +1,5 @@ import fs from 'node:fs' -import path from 'path' +import path from 'node:path' import boxen from 'boxen' import execa from 'execa' @@ -7,11 +7,24 @@ import execa from 'execa' import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' import { errorTelemetry } from '@cedarjs/telemetry' +// @ts-expect-error - Types not available for JS files import c from '../lib/colors.js' +// @ts-expect-error - Types not available for JS files import { getPaths } from '../lib/index.js' -// eslint-disable-next-line no-unused-vars -export const handler = async ({ _, $0, commands = [], ...options }) => { +interface PrismaOptions { + _?: string[] + $0?: string + commands?: string[] + [key: string]: unknown +} + +export const handler = async ({ + _ = [], + $0: _0 = '', + commands = [], + ...options +}: PrismaOptions) => { recordTelemetryAttributes({ command: 'prisma', }) @@ -40,14 +53,16 @@ export const handler = async ({ _, $0, commands = [], ...options }) => { } // Convert command and options into a string that's run via execa - const args = commands + const args: (string | number)[] = [...commands] for (const [name, value] of Object.entries(options)) { // Allow both long and short form commands, e.g. --name and -n args.push(name.length > 1 ? `--${name}` : `-${name}`) if (typeof value === 'string') { - // Make sure options that take multiple quoted words - // like `-n "create user"` are passed to prisma with quotes. - value.split(' ').length > 1 ? args.push(`"${value}"`) : args.push(value) + if (value.split(' ').length > 1) { + args.push(`"${value}"`) + } else { + args.push(value) + } } else if (typeof value === 'number') { args.push(value) } @@ -60,7 +75,7 @@ export const handler = async ({ _, $0, commands = [], ...options }) => { try { const prismaBin = path.join(rwjsPaths.base, 'node_modules/.bin/prisma') - execa.sync(prismaBin, args, { + execa.sync(prismaBin, args as string[], { cwd: rwjsPaths.base, stdio: 'inherit', cleanup: true, @@ -69,9 +84,19 @@ export const handler = async ({ _, $0, commands = [], ...options }) => { if (hasHelpOption || commands.length === 0) { printWrapInfo() } - } catch (e) { - errorTelemetry(process.argv, `Error generating prisma client: ${e.message}`) - process.exit(e?.exitCode || 1) + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) + errorTelemetry(process.argv, `Error generating prisma client: ${message}`) + + if ( + e instanceof Object && + 'exitCode' in e && + typeof e.exitCode === 'number' + ) { + process.exit(e.exitCode) + } else { + process.exit(1) + } } } diff --git a/packages/cli/src/commands/test.js b/packages/cli/src/commands/test.ts similarity index 76% rename from packages/cli/src/commands/test.js rename to packages/cli/src/commands/test.ts index 0e0c95c674..89d4222ff2 100644 --- a/packages/cli/src/commands/test.js +++ b/packages/cli/src/commands/test.ts @@ -1,18 +1,23 @@ import { terminalLink } from 'termi-link' +import type { Argv } from 'yargs' +// @ts-expect-error - No types for .js files import c from '../lib/colors.js' +// @ts-expect-error - No types for .js files import { sides } from '../lib/project.js' export const command = 'test [filter..]' export const description = 'Run Jest tests. Defaults to watch mode' -export const builder = (yargs) => { + +export const builder = (yargs: Argv) => { yargs .strict(false) // so that we can forward arguments to jest .positional('filter', { default: sides(), description: 'Which side(s) to test, and/or a regular expression to match against your test files to filter by', - type: 'array', + type: 'string', + array: true, }) .option('watch', { describe: @@ -42,7 +47,16 @@ export const builder = (yargs) => { ) } -export const handler = async (options) => { +interface TestOptions { + filter: string[] + watch: boolean + collectCoverage: boolean + dbPush: boolean + [key: string]: unknown +} + +export const handler = async (options: TestOptions) => { + // @ts-expect-error - testHandler is not typed const { handler } = await import('./testHandler.js') return handler(options) } diff --git a/packages/cli/src/commands/type-check.js b/packages/cli/src/commands/type-check.ts similarity index 76% rename from packages/cli/src/commands/type-check.js rename to packages/cli/src/commands/type-check.ts index a23fb2f173..286e9eb295 100644 --- a/packages/cli/src/commands/type-check.js +++ b/packages/cli/src/commands/type-check.ts @@ -1,17 +1,21 @@ import { terminalLink } from 'termi-link' +import type { Argv } from 'yargs' +// @ts-expect-error - No types for .js files import { sides } from '../lib/project.js' export const command = 'type-check [sides..]' export const aliases = ['tsc', 'tc'] export const description = 'Run a TypeScript compiler check on your project' -export const builder = (yargs) => { + +export const builder = (yargs: Argv) => { yargs .strict(false) // so that we can forward arguments to tsc .positional('sides', { default: sides(), description: 'Which side(s) to run a typecheck on', - type: 'array', + type: 'string', + array: true, }) .option('prisma', { type: 'boolean', @@ -37,7 +41,14 @@ export const builder = (yargs) => { ) } -export const handler = async (options) => { +interface TypeCheckOptions { + sides: string[] + verbose: boolean + prisma: boolean + generate: boolean +} + +export const handler = async (options: TypeCheckOptions) => { const { handler } = await import('./type-checkHandler.js') return handler(options) } diff --git a/packages/cli/src/commands/type-checkHandler.js b/packages/cli/src/commands/type-checkHandler.ts similarity index 62% rename from packages/cli/src/commands/type-checkHandler.js rename to packages/cli/src/commands/type-checkHandler.ts index 2e16175e8e..de65dd0480 100644 --- a/packages/cli/src/commands/type-checkHandler.js +++ b/packages/cli/src/commands/type-checkHandler.ts @@ -1,4 +1,4 @@ -import path from 'path' +import path from 'node:path' import concurrently from 'concurrently' import execa from 'execa' @@ -6,10 +6,24 @@ import { Listr } from 'listr2' import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' +// @ts-expect-error - No types for .js files import { generatePrismaClient } from '../lib/generatePrismaClient.js' +// @ts-expect-error - No types for .js files import { getPaths } from '../lib/index.js' -export const handler = async ({ sides, verbose, prisma, generate }) => { +interface TypeCheckOptions { + sides: string[] + verbose: boolean + prisma: boolean + generate: boolean +} + +export const handler = async ({ + sides, + verbose, + prisma, + generate, +}: TypeCheckOptions) => { recordTelemetryAttributes({ command: 'type-check', sides: JSON.stringify(sides), @@ -38,11 +52,24 @@ export const handler = async ({ sides, verbose, prisma, generate }) => { }) try { await result - } catch (err) { - if (err.length) { + } catch (err: unknown) { + if (Array.isArray(err)) { // Non-null exit codes - const exitCodes = err.map((e) => e?.exitCode).filter(Boolean) - conclusiveExitCode = Math.max(...exitCodes) + const exitCodes = err + .map((e: unknown) => + e instanceof Object && + 'exitCode' in e && + typeof e.exitCode === 'number' + ? e.exitCode + : undefined, + ) + .filter( + (code: number | undefined): code is number => + code !== undefined && code !== null, + ) + if (exitCodes.length > 0) { + conclusiveExitCode = Math.max(...exitCodes) + } } } @@ -68,7 +95,7 @@ export const handler = async ({ sides, verbose, prisma, generate }) => { }, ], { - renderer: verbose && 'verbose', + renderer: verbose ? 'verbose' : 'default', rendererOptions: { collapseSubtasks: false }, }, ).run() diff --git a/packages/cli/src/commands/upgrade.js b/packages/cli/src/commands/upgrade.ts similarity index 84% rename from packages/cli/src/commands/upgrade.js rename to packages/cli/src/commands/upgrade.ts index 85b7e8f8f3..6d98864573 100644 --- a/packages/cli/src/commands/upgrade.js +++ b/packages/cli/src/commands/upgrade.ts @@ -9,19 +9,24 @@ import latestVersion from 'latest-version' import { Listr } from 'listr2' import semver from 'semver' import { terminalLink } from 'termi-link' +import type { Argv } from 'yargs' import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' import { getConfig } from '@cedarjs/project-config' +// @ts-expect-error - Types not available for JS files import c from '../lib/colors.js' +// @ts-expect-error - Types not available for JS files import { generatePrismaClient } from '../lib/generatePrismaClient.js' +// @ts-expect-error - Types not available for JS files import { getPaths } from '../lib/index.js' +// @ts-expect-error - Types not available for JS files import { PLUGIN_CACHE_FILENAME } from '../lib/plugin.js' export const command = 'upgrade' export const description = 'Upgrade all @cedarjs packages via interactive CLI' -export const builder = (yargs) => { +export const builder = (yargs: Argv) => { yargs .example( 'cedar upgrade -t 0.20.1-canary.5', @@ -83,15 +88,15 @@ export const builder = (yargs) => { const SEMVER_REGEX = /(?<=^v?|\sv?)(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*)(?:\.(?:0|[1-9]\d*|[\da-z-]*[a-z-][\da-z-]*))*)?(?:\+[\da-z-]+(?:\.[\da-z-]+)*)?(?=$|\s)/i -const isValidSemver = (string) => { +const isValidSemver = (string: string) => { return SEMVER_REGEX.test(string) } -const isValidCedarJSTag = (tag) => { +const isValidCedarJSTag = (tag: string) => { return ['rc', 'canary', 'latest', 'next', 'experimental'].includes(tag) } -export const validateTag = (tag) => { +export const validateTag = (tag: string) => { const isTagValid = isValidSemver(tag) || isValidCedarJSTag(tag) if (!isTagValid) { @@ -106,15 +111,43 @@ export const validateTag = (tag) => { return tag } -export const handler = async ({ dryRun, tag, verbose, dedupe, yes, force }) => { +interface ExecaError extends Error { + stdout?: string + stderr?: string + exitCode?: number +} + +function isExecaError(e: unknown): e is ExecaError { + return ( + e instanceof Error && ('stdout' in e || 'stderr' in e || 'exitCode' in e) + ) +} + +interface UpgradeOptions { + dryRun?: boolean + tag?: string + verbose?: boolean + dedupe?: boolean + yes?: boolean + force?: boolean +} + +export const handler = async ({ + dryRun, + tag, + verbose, + dedupe, + yes, + force, +}: UpgradeOptions) => { recordTelemetryAttributes({ command: 'upgrade', - dryRun, - tag, - verbose, - dedupe, - yes, - force, + dryRun: !!dryRun, + tag: tag ?? 'latest', + verbose: !!verbose, + dedupe: !!dedupe, + yes: !!yes, + force: !!force, }) let preUpgradeMessage = '' @@ -146,9 +179,14 @@ export const handler = async ({ dryRun, tag, verbose, dedupe, yes, force }) => { 'This will upgrade your CedarJS project to the latest version. Do you want to proceed?', initial: 'Y', default: '(Yes/no)', - format: function (value) { + format: function ( + // Enquirer state is not easily typed here, and 'this' is used + // to access it. + this: any, + value: unknown, + ) { if (this.state.submitted) { - return this.isTrue(value) ? 'yes' : 'no' + return this.isTrue(value) ? 'no' : 'yes' } return 'Yes' @@ -205,15 +243,15 @@ export const handler = async ({ dryRun, tag, verbose, dedupe, yes, force }) => { }, { title: 'Running yarn install', - task: (ctx) => yarnInstall(ctx, { dryRun, verbose }), + task: () => yarnInstall({ dryRun, verbose }), enabled: (ctx) => !ctx.preUpgradeError, - skip: () => dryRun, + skip: () => !!dryRun, }, { title: 'Refreshing the Prisma client', task: (_ctx, task) => refreshPrismaClient(task, { verbose }), enabled: (ctx) => !ctx.preUpgradeError, - skip: () => dryRun, + skip: () => !!dryRun, }, { title: 'De-duplicating dependencies', @@ -234,7 +272,7 @@ export const handler = async ({ dryRun, tag, verbose, dedupe, yes, force }) => { if ([undefined, 'latest', 'rc'].includes(tag)) { const ghReleasesLink = terminalLink( `GitHub Release notes`, - `https://github.com/cedarjs/cedar/releases`, // intentionally not linking to specific version + `https://github.com/cedarjs/cedar/releases`, ) const discordLink = terminalLink( `Discord`, @@ -299,14 +337,22 @@ export const handler = async ({ dryRun, tag, verbose, dedupe, yes, force }) => { } } -async function yarnInstall({ verbose }) { +async function yarnInstall({ + verbose, + dryRun, +}: { + verbose?: boolean + dryRun?: boolean +}) { try { + // Unused dryRun argument for consistency? + void dryRun await execa('yarn install', { shell: true, stdio: verbose ? 'inherit' : 'pipe', cwd: getPaths().base, }) - } catch (e) { + } catch { throw new Error( 'Could not finish installation. Please run `yarn install` and then `yarn dedupe`, before continuing', ) @@ -317,7 +363,10 @@ async function yarnInstall({ verbose }) { * Removes the CLI plugin cache. This prevents the CLI from using outdated versions of the plugin, * when the plugins share the same alias. e.g. `cedar sb` used to point to `@cedarjs/cli-storybook` but now points to `@cedarjs/cli-storybook-vite` */ -async function removeCliCache(ctx, { dryRun, verbose }) { +async function removeCliCache( + ctx: Record, + { dryRun, verbose }: { dryRun?: boolean; verbose?: boolean }, +) { const cliCacheDir = path.join( getPaths().generated.base, PLUGIN_CACHE_FILENAME, @@ -332,7 +381,10 @@ async function removeCliCache(ctx, { dryRun, verbose }) { } } -async function setLatestVersionToContext(ctx, tag) { +async function setLatestVersionToContext( + ctx: Record, + tag?: string, +) { try { const foundVersion = await latestVersion( '@cedarjs/core', @@ -341,7 +393,7 @@ async function setLatestVersionToContext(ctx, tag) { ctx.versionToUpgradeTo = foundVersion return foundVersion - } catch (e) { + } catch { if (tag) { throw new Error('Could not find the latest `' + tag + '` version') } @@ -354,12 +406,17 @@ async function setLatestVersionToContext(ctx, tag) { * Iterates over CedarJS dependencies in package.json files and updates the * version. */ -function updatePackageJsonVersion(pkgPath, version, task, { dryRun, verbose }) { +function updatePackageJsonVersion( + pkgPath: string, + version: string, + task: { title: string }, + { dryRun, verbose }: { dryRun?: boolean; verbose?: boolean }, +) { const pkg = JSON.parse( fs.readFileSync(path.join(pkgPath, 'package.json'), 'utf-8'), ) - const messages = [] + const messages: string[] = [] if (pkg.dependencies) { for (const depName of Object.keys(pkg.dependencies).filter( @@ -401,7 +458,10 @@ function updatePackageJsonVersion(pkgPath, version, task, { dryRun, verbose }) { } } -function updateCedarJSDepsForAllSides(ctx, options) { +function updateCedarJSDepsForAllSides( + ctx: Record, + options: { dryRun?: boolean; verbose?: boolean }, +) { if (!ctx.versionToUpgradeTo) { throw new Error('Failed to upgrade') } @@ -420,7 +480,7 @@ function updateCedarJSDepsForAllSides(ctx, options) { task: (_ctx, task) => updatePackageJsonVersion( basePath, - ctx.versionToUpgradeTo, + String(ctx.versionToUpgradeTo), task, options, ), @@ -430,7 +490,10 @@ function updateCedarJSDepsForAllSides(ctx, options) { ) } -async function updatePackageVersionsFromTemplate(ctx, { dryRun, verbose }) { +async function updatePackageVersionsFromTemplate( + ctx: Record, + { dryRun, verbose }: { dryRun?: boolean; verbose?: boolean }, +) { if (!ctx.versionToUpgradeTo) { throw new Error('Failed to upgrade') } @@ -464,10 +527,10 @@ async function updatePackageVersionsFromTemplate(ctx, { dryRun, verbose }) { const localPackageJsonText = fs.readFileSync(pkgJsonPath, 'utf-8') const localPackageJson = JSON.parse(localPackageJsonText) - const messages = [] + const messages: string[] = [] Object.entries(templatePackageJson.dependencies || {}).forEach( - ([depName, depVersion]) => { + ([depName, depVersion]: [string, unknown]) => { // CedarJS packages are handled in another task if (!depName.startsWith('@cedarjs/')) { if (verbose || dryRun) { @@ -482,7 +545,7 @@ async function updatePackageVersionsFromTemplate(ctx, { dryRun, verbose }) { ) Object.entries(templatePackageJson.devDependencies || {}).forEach( - ([depName, depVersion]) => { + ([depName, depVersion]: [string, unknown]) => { // CedarJS packages are handled in another task if (!depName.startsWith('@cedarjs/')) { if (verbose || dryRun) { @@ -513,7 +576,10 @@ async function updatePackageVersionsFromTemplate(ctx, { dryRun, verbose }) { ) } -async function downloadYarnPatches(ctx, { dryRun, verbose }) { +async function downloadYarnPatches( + ctx: Record, + { dryRun, verbose }: { dryRun?: boolean; verbose?: boolean }, +) { if (!ctx.versionToUpgradeTo) { throw new Error('Failed to upgrade') } @@ -527,7 +593,7 @@ async function downloadYarnPatches(ctx, { dryRun, verbose }) { 'https://api.github.com/repos/cedarjs/cedar/git/trees/main?recursive=1', { headers: { - Authorization: githubToken ? `Bearer ${githubToken}` : undefined, + ...(githubToken && { Authorization: `Bearer ${githubToken}` }), ['X-GitHub-Api-Version']: '2022-11-28', Accept: 'application/vnd.github+json', }, @@ -535,10 +601,11 @@ async function downloadYarnPatches(ctx, { dryRun, verbose }) { ) const json = await res.json() - const patches = json.tree?.filter((patchInfo) => - patchInfo.path.startsWith( - 'packages/create-cedar-app/templates/ts/.yarn/patches/', - ), + const patches: { path: string; url: string }[] = json.tree?.filter( + (patchInfo: { path: string }) => + patchInfo.path.startsWith( + 'packages/create-cedar-app/templates/ts/.yarn/patches/', + ), ) const patchDir = path.join(getPaths().base, '.yarn', 'patches') @@ -578,7 +645,10 @@ async function downloadYarnPatches(ctx, { dryRun, verbose }) { ) } -async function refreshPrismaClient(task, { verbose }) { +async function refreshPrismaClient( + task: { skip: (msg: string) => void }, + { verbose }: { verbose?: boolean }, +) { // Relates to prisma/client issue // See: https://github.com/redwoodjs/redwood/issues/1083 try { @@ -586,16 +656,20 @@ async function refreshPrismaClient(task, { verbose }) { verbose, force: false, }) - } catch (e) { + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) task.skip('Refreshing the Prisma client caused an Error.') console.log( 'You may need to update your prisma client manually: $ yarn cedar prisma generate', ) - console.log(c.error(e.message)) + console.log(c.error(message)) } } -const dedupeDeps = async (_task, { verbose }) => { +const dedupeDeps = async ( + _task: unknown, + { verbose }: { verbose?: boolean }, +) => { try { await execa('yarn dedupe', { shell: true, @@ -603,27 +677,38 @@ const dedupeDeps = async (_task, { verbose }) => { cwd: getPaths().base, }) } catch (e) { - console.log(c.error(e.message)) + // ExecaError is an instance of Error + const message = e instanceof Error ? e.message : String(e) + console.log(c.error(message)) throw new Error( 'Could not finish de-duplication. For yarn 1.x, please run `npx yarn-deduplicate`, or for yarn >= 3 run `yarn dedupe` before continuing', ) } + await yarnInstall({ verbose }) } // exported for testing -export async function runPreUpgradeScripts(ctx, task, { verbose, force }) { +export async function runPreUpgradeScripts( + ctx: Record, + task: { output: unknown }, + { verbose, force }: { verbose?: boolean; force?: boolean }, +) { if (!ctx.versionToUpgradeTo) { return } - const version = ctx.versionToUpgradeTo + const version = + typeof ctx.versionToUpgradeTo === 'string' + ? ctx.versionToUpgradeTo + : undefined + const parsed = semver.parse(version) const baseUrl = 'https://raw.githubusercontent.com/cedarjs/cedar/main/upgrade-scripts/' const manifestUrl = `${baseUrl}manifest.json` - let manifest = [] + let manifest: string[] = [] try { const res = await fetch(manifestUrl) @@ -644,7 +729,7 @@ export async function runPreUpgradeScripts(ctx, task, { verbose, force }) { return } - const checkLevels = [] + const checkLevels: { id: string; candidates: string[] }[] = [] if (parsed && !parsed.prerelease.length) { // 1. Exact match: 3.4.1 checkLevels.push({ @@ -677,7 +762,7 @@ export async function runPreUpgradeScripts(ctx, task, { verbose, force }) { }) } - const scriptsToRun = [] + const scriptsToRun: string[] = [] // Find all existing scripts (one per level) using the manifest for (const level of checkLevels) { @@ -814,7 +899,7 @@ export async function runPreUpgradeScripts(ctx, task, { verbose, force }) { try { const { stdout } = await execa( 'node', - ['script.mts', '--verbose', verbose, '--force', force], + ['script.mts', '--verbose', String(verbose), '--force', String(force)], { cwd: tempDir }, ) @@ -825,19 +910,27 @@ export async function runPreUpgradeScripts(ctx, task, { verbose, force }) { ctx.preUpgradeMessage += `\n${stdout}` } - } catch (e) { - const errorOutput = e.stdout || e.stderr || e.message || '' - const verboseErrorMessage = verbose - ? `Pre-upgrade check ${scriptName} failed with exit code ${e.exitCode}:\n` + - `${e.stderr ? e.stderr + '\n' : ''}` - : '' + } catch (e: unknown) { + let errorOutput = String(e) + let exitCode: number | undefined + let stderr: string | undefined + + if (isExecaError(e)) { + errorOutput = e.stdout || e.message + stderr = e.stderr + exitCode = e.exitCode + } else if (e instanceof Error) { + errorOutput = e.message + } if (ctx.preUpgradeError) { ctx.preUpgradeError += '\n' } if (verbose) { - ctx.preUpgradeError += `\n${verboseErrorMessage}` + ctx.preUpgradeError += `\nPre-upgrade check ${scriptName} failed with exit code ${exitCode}:\n${ + stderr ? stderr + '\n' : '' + }` } ctx.preUpgradeError += `\n${errorOutput}` @@ -857,7 +950,7 @@ export async function runPreUpgradeScripts(ctx, task, { verbose, force }) { } } -const extractDependencies = (content) => { +const extractDependencies = (content: string) => { const deps = new Map() // 1. Explicit dependencies via comments diff --git a/packages/cli/src/lib/__tests__/getDevNodeOptions.test.ts b/packages/cli/src/lib/__tests__/getDevNodeOptions.test.ts index ce4aacf4ab..4dea481b1b 100644 --- a/packages/cli/src/lib/__tests__/getDevNodeOptions.test.ts +++ b/packages/cli/src/lib/__tests__/getDevNodeOptions.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest' -import { getDevNodeOptions } from '../../commands/devHandler' +import { getDevNodeOptions } from '../../commands/devHandler.js' describe('getNodeOptions', () => { const enableSourceMapsOption = '--enable-source-maps' diff --git a/packages/cli/src/testLib/cells.ts b/packages/cli/src/testLib/cells.ts index 98ccad2ef8..2bb9e2157a 100644 --- a/packages/cli/src/testLib/cells.ts +++ b/packages/cli/src/testLib/cells.ts @@ -5,6 +5,7 @@ import { types } from '@babel/core' import type { ParserPlugin } from '@babel/parser' import { parse as babelParse } from '@babel/parser' import traverse from '@babel/traverse' +import type { NodePath } from '@babel/traverse' import fg from 'fast-glob' import type { DocumentNode, @@ -73,7 +74,7 @@ export const isFileInsideFolder = (filePath: string, folderPath: string) => { export const hasDefaultExport = (ast: types.Node): boolean => { let exported = false - traverse(ast, { + traverse.default(ast, { ExportDefaultDeclaration() { exported = true return @@ -89,8 +90,8 @@ interface NamedExports { export const getNamedExports = (ast: types.Node): NamedExports[] => { const namedExports: NamedExports[] = [] - traverse(ast, { - ExportNamedDeclaration(path) { + traverse.default(ast, { + ExportNamedDeclaration(path: NodePath) { // Re-exports from other modules // Eg: export { a, b } from './module.js' const specifiers = path.node?.specifiers @@ -123,7 +124,7 @@ export const getNamedExports = (ast: types.Node): NamedExports[] => { }) } else if (declaration.type === 'ClassDeclaration') { namedExports.push({ - name: declaration?.id?.name, + name: declaration?.id?.name as string, type: 'class', }) } @@ -162,19 +163,21 @@ export const fileToAst = (filePath: string): types.Node => { export const getCellGqlQuery = (ast: types.Node) => { let cellQuery: string | undefined = undefined - traverse(ast, { - ExportNamedDeclaration({ node }) { + traverse.default(ast, { + ExportNamedDeclaration({ node }: NodePath) { if ( node.exportKind === 'value' && types.isVariableDeclaration(node.declaration) ) { - const exportedQueryNode = node.declaration.declarations.find((d) => { - return ( - types.isIdentifier(d.id) && - d.id.name === 'QUERY' && - types.isTaggedTemplateExpression(d.init) - ) - }) + const exportedQueryNode = node.declaration.declarations.find( + (d: types.VariableDeclarator) => { + return ( + types.isIdentifier(d.id) && + d.id.name === 'QUERY' && + types.isTaggedTemplateExpression(d.init) + ) + }, + ) if (exportedQueryNode) { const templateExpression = diff --git a/packages/cli/src/testUtils/index.ts b/packages/cli/src/testUtils/index.ts index 0b2f6a2271..7f60eb8f98 100644 --- a/packages/cli/src/testUtils/index.ts +++ b/packages/cli/src/testUtils/index.ts @@ -4,8 +4,6 @@ import parserBabel from 'prettier/parser-babel' export const formatCode = async (code: string) => { return format(code, { parser: 'babel-ts', - // @ts-expect-error - TS is picking up @types/babel, which is outdated. - // We have it because babel-plugin-tester pulls it in plugins: [parserBabel], }) } diff --git a/packages/cli/src/testUtils/matchFolderTransform.ts b/packages/cli/src/testUtils/matchFolderTransform.ts index 97adfdf997..b88253d6fd 100644 --- a/packages/cli/src/testUtils/matchFolderTransform.ts +++ b/packages/cli/src/testUtils/matchFolderTransform.ts @@ -37,8 +37,6 @@ async function getFormatCode() { formatCodeCache = async (code: string) => { return format(code, { parser: 'babel-ts', - // @ts-expect-error - TS is picking up @types/babel, which is outdated. - // We have it because babel-plugin-tester pulls it in plugins: [parserBabel.default], }) } diff --git a/packages/cli/tsconfig.build.json b/packages/cli/tsconfig.build.json index 83abaf1356..3b91e94afa 100644 --- a/packages/cli/tsconfig.build.json +++ b/packages/cli/tsconfig.build.json @@ -3,7 +3,7 @@ "compilerOptions": { "rootDir": "src", "outDir": "dist", - "module": "Node16", + "module": "Node20", "moduleResolution": "Node16" }, "include": ["src", "./testUtils.d.ts"], diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 24bdc57366..3f32a5a6e2 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -3,7 +3,9 @@ "compilerOptions": { "isolatedModules": true, "emitDeclarationOnly": false, - "noEmit": true + "noEmit": true, + "module": "NodeNext", + "moduleResolution": "NodeNext" }, "include": ["."], "exclude": ["**/__testfixtures__"],