From 28c0e6e0ff971ec66c92194040bb63c33da78410 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 28 Dec 2025 23:49:55 +0100 Subject: [PATCH 1/8] chore(cli): Convert JS files to TS (part II) --- GEMINI.md | 2 +- .../{build.test.js => build.test.ts} | 25 ++- .../cli/src/commands/__tests__/exec.test.ts | 1 - .../__tests__/{info.test.js => info.test.ts} | 0 .../{prisma.test.js => prisma.test.ts} | 20 ++- .../cli/src/commands/__tests__/test.test.js | 145 --------------- .../cli/src/commands/__tests__/test.test.ts | 163 +++++++++++++++++ ...{type-check.test.js => type-check.test.ts} | 41 +++-- .../src/commands/__tests__/upgrade.test.ts | 14 +- .../cli/src/commands/{build.js => build.ts} | 12 +- .../{buildHandler.js => buildHandler.ts} | 15 +- .../cli/src/commands/{check.js => check.ts} | 4 +- .../src/commands/{console.js => console.ts} | 4 +- .../{consoleHandler.js => consoleHandler.ts} | 27 ++- .../src/commands/{destroy.js => destroy.ts} | 13 +- packages/cli/src/commands/dev.ts | 2 +- packages/cli/src/commands/devHandler.ts | 15 +- .../cli/src/commands/{exec.js => exec.ts} | 5 +- .../{execHandler.js => execHandler.ts} | 53 ++++-- .../{experimental.js => experimental.ts} | 9 +- .../src/commands/{generate.js => generate.ts} | 32 +++- .../cli/src/commands/{info.js => info.ts} | 5 +- .../cli/src/commands/{lint.js => lint.ts} | 46 +++-- .../commands/{prerender.js => prerender.ts} | 12 +- ...rerenderHandler.js => prerenderHandler.ts} | 144 +++++++-------- .../cli/src/commands/{prisma.js => prisma.ts} | 6 +- .../{prismaHandler.js => prismaHandler.ts} | 53 ++++-- .../cli/src/commands/{test.js => test.ts} | 20 ++- .../commands/{type-check.js => type-check.ts} | 17 +- ...e-checkHandler.js => type-checkHandler.ts} | 40 ++++- .../src/commands/{upgrade.js => upgrade.ts} | 167 +++++++++++++----- 31 files changed, 694 insertions(+), 418 deletions(-) rename packages/cli/src/commands/__tests__/{build.test.js => build.test.ts} (75%) rename packages/cli/src/commands/__tests__/{info.test.js => info.test.ts} (100%) rename packages/cli/src/commands/__tests__/{prisma.test.js => prisma.test.ts} (81%) delete mode 100644 packages/cli/src/commands/__tests__/test.test.js create mode 100644 packages/cli/src/commands/__tests__/test.test.ts rename packages/cli/src/commands/__tests__/{type-check.test.js => type-check.test.ts} (73%) rename packages/cli/src/commands/{build.js => build.ts} (77%) rename packages/cli/src/commands/{buildHandler.js => buildHandler.ts} (92%) rename packages/cli/src/commands/{check.js => check.ts} (84%) rename packages/cli/src/commands/{console.js => console.ts} (69%) rename packages/cli/src/commands/{consoleHandler.js => consoleHandler.ts} (73%) rename packages/cli/src/commands/{destroy.js => destroy.ts} (71%) rename packages/cli/src/commands/{exec.js => exec.ts} (87%) rename packages/cli/src/commands/{execHandler.js => execHandler.ts} (76%) rename packages/cli/src/commands/{experimental.js => experimental.ts} (75%) rename packages/cli/src/commands/{generate.js => generate.ts} (68%) rename packages/cli/src/commands/{info.js => info.ts} (90%) rename packages/cli/src/commands/{lint.js => lint.ts} (78%) rename packages/cli/src/commands/{prerender.js => prerender.ts} (75%) rename packages/cli/src/commands/{prerenderHandler.js => prerenderHandler.ts} (69%) rename packages/cli/src/commands/{prisma.js => prisma.ts} (79%) rename packages/cli/src/commands/{prismaHandler.js => prismaHandler.ts} (68%) rename packages/cli/src/commands/{test.js => test.ts} (78%) rename packages/cli/src/commands/{type-check.js => type-check.ts} (76%) rename packages/cli/src/commands/{type-checkHandler.js => type-checkHandler.ts} (61%) rename packages/cli/src/commands/{upgrade.js => upgrade.ts} (85%) diff --git a/GEMINI.md b/GEMINI.md index b7d0227716..056fc8520e 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -71,7 +71,7 @@ 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`. - **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/src/commands/__tests__/build.test.js b/packages/cli/src/commands/__tests__/build.test.ts similarity index 75% rename from packages/cli/src/commands/__tests__/build.test.js rename to packages/cli/src/commands/__tests__/build.test.ts index 270185e454..a7c8388a9f 100644 --- a/packages/cli/src/commands/__tests__/build.test.js +++ b/packages/cli/src/commands/__tests__/build.test.ts @@ -1,5 +1,12 @@ +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('@cedarjs/project-config', async (importOriginal) => { - const originalProjectConfig = await importOriginal() + const originalProjectConfig = await importOriginal() return { ...originalProjectConfig, getPaths: () => { @@ -23,14 +30,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,9 +48,7 @@ vi.mock('node:fs', async () => { } }) -import { Listr } from 'listr2' -import { vi, afterEach, test, expect } from 'vitest' - +// @ts-expect-error - Mocking Listr vi.mock('listr2') // Make sure prerender doesn't get triggered @@ -62,7 +67,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(` + // @ts-expect-error - Listr is mocked + expect(vi.mocked(Listr).mock.calls[0][0].map((x: { title: string }) => x.title)).toMatchInlineSnapshot(` [ "Generating Prisma Client...", "Verifying graphql schema...", @@ -80,7 +86,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(` + // @ts-expect-error - Listr is mocked + expect(vi.mocked(Listr).mock.calls[0][0].map((x: { title: string }) => 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..5ed7dc347c --- /dev/null +++ b/packages/cli/src/commands/__tests__/test.test.ts @@ -0,0 +1,163 @@ +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() +}) + +test('Runs tests for all available sides if no filter passed', async () => { + await handler({} as any /* simplified for test input */) + + 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( + { + filter: ['api'], + dbPush: true, + } as any /* simplified for test input */, + ) + + 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( + { + filter: ['api'], + dbPush: false, + } as any /* simplified for test input */, + ) + + 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( + { + filter: ['bazinga'], + } as any /* simplified for test input */, + ) + + 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( + { + filter: ['web', 'bazinga'], + } as any /* simplified for test input */, + ) + + 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( + { + filter: ['web'], + } as any /* simplified for test input */, + ) + + 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( + { + filter: ['web', 'bazinga'], + } as any /* simplified for test input */, + ) + + 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( + { + u: true, + debug: true, + json: true, + collectCoverage: true, + } as any /* simplified for test input */, + ) + + 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( + { + bazinga: false, + hello: 'world', + } as any /* simplified for test input */, + ) + + // 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 73% rename from packages/cli/src/commands/__tests__/type-check.test.js rename to packages/cli/src/commands/__tests__/type-check.test.ts index fd22f8f12a..1a2676d7b3 100644 --- a/packages/cli/src/commands/__tests__/type-check.test.js +++ b/packages/cli/src/commands/__tests__/type-check.test.ts @@ -1,3 +1,12 @@ +import path from 'node:path' + +import concurrently from 'concurrently' +import execa from 'execa' +import { vi, beforeEach, afterEach, test, expect } from 'vitest' +import type * as Lib from '../../lib' + +import '../../lib/mockTelemetry' + vi.mock('execa', () => ({ default: vi.fn((cmd, params, options) => { return { @@ -15,20 +24,18 @@ 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 +50,7 @@ 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 { runCommandTask } from '../../lib/index.js' import { handler } from '../type-check.js' @@ -59,8 +61,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 +70,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 +96,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 +109,8 @@ 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..7f5d2f4310 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 = { @@ -141,7 +139,7 @@ describe('runPreUpgradeScripts', () => { ` // Mock readFile to return the script content - vi.mocked(fs.promises.readFile).mockResolvedValue(scriptContent as any) + vi.mocked(fs.promises.readFile).mockResolvedValue(scriptContent as any /* simplified for test mock */) vi.mocked(fetch).mockImplementation(async (url: string | URL | Request) => { if (url.toString().endsWith('/manifest.json')) { @@ -176,12 +174,12 @@ describe('runPreUpgradeScripts', () => { return { stdout: '', stderr: '', - } as unknown as execa.ExecaChildProcess + } as any /* simplified for test mock */ } else if (command === 'node') { return { stdout: 'Upgrade check passed', stderr: '', - } as unknown as execa.ExecaChildProcess + } as any /* simplified for test mock */ } throw new Error(`Unexpected command: ${command} ${actualArgs?.join(' ')}`) @@ -225,7 +223,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 92% rename from packages/cli/src/commands/buildHandler.js rename to packages/cli/src/commands/buildHandler.ts index 6556870f09..6b740a8650 100644 --- a/packages/cli/src/commands/buildHandler.js +++ b/packages/cli/src/commands/buildHandler.ts @@ -13,15 +13,24 @@ 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), @@ -133,7 +142,7 @@ export const handler = async ({ } }, }, - ].filter(Boolean) + ].filter((x): x is Exclude => !!x) const triggerPrerender = async () => { console.log('Starting prerendering...') @@ -157,7 +166,9 @@ export const handler = async ({ }) } + // @ts-expect-error - tasks might have incompatible types for Listr 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 73% rename from packages/cli/src/commands/consoleHandler.js rename to packages/cli/src/commands/consoleHandler.ts index 59944b82eb..041d4b4a88 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,29 @@ const loadPrismaClient = (replContext) => { } const consoleHistoryFile = path.join(paths.generated.base, 'console_history') -const persistConsoleHistory = (r) => { + +interface REPLServerWithHistory extends repl.REPLServer { + history: string[] + lines: string[] +} + +const persistConsoleHistory = (r: repl.REPLServer) => { + const lines = (r as REPLServerWithHistory).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) { + .map((line) => (r as any /* history is not in Node REPL types but exists in implementation */).history.push(line)) + } catch { // We can ignore this -- it just means the user doesn't have any history yet } } @@ -64,17 +72,18 @@ export const handler = () => { // always await promises. // source: https://github.com/nodejs/node/issues/13209#issuecomment-619526317 const defaultEval = r.eval + // @ts-expect-error - overriding eval signature r.eval = (cmd, context, filename, callback) => { defaultEval(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..24ad4c5ac1 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)}`, 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 76% rename from packages/cli/src/commands/execHandler.js rename to packages/cli/src/commands/execHandler.ts index d3100bf12c..bf95eefd8f 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,7 +49,15 @@ 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, @@ -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 @@ -87,7 +104,7 @@ export const handler = async (args) => { if (!scriptPath) { console.error( - c.error(`\nNo script called \`${name}\` in the ./scripts folder.\n`), + c.error(`\nNo script called \`\${name}\` in the ./scripts folder.\n`), ) printAvailableScriptsToConsole() @@ -114,8 +131,9 @@ export const handler = async (args) => { functionName: 'default', args: { args: scriptArgs }, }) - } catch (e) { - console.error(c.error(`Error in script: ${e.message}`)) + } catch (e: unknown) { + const message = e instanceof Error ? e.message : String(e) + console.error(c.error(`Error in script: ${message}`)) throw e } }, @@ -124,6 +142,7 @@ export const handler = async (args) => { const tasks = new Listr(scriptTasks, { rendererOptions: { collapseSubtasks: false }, + // @ts-expect-error - renderer might be incompatible renderer: args.silent ? 'silent' : 'verbose', }) @@ -133,7 +152,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 +163,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 +178,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.', ), ) @@ -172,4 +191,4 @@ function resolveScriptPath(name) { } return null -} +} \ No newline at end of file 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 78% rename from packages/cli/src/commands/lint.js rename to packages/cli/src/commands/lint.ts index e12b428fdc..eb58ef794d 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,25 @@ export const builder = (yargs) => { ) } -export const handler = async ({ path, fix, format }) => { +interface LintOptions { + path?: string[] + fix?: boolean + format?: string +} + +export const handler = async ({ path: filePath, fix, format }: LintOptions) => { recordTelemetryAttributes({ command: 'lint', fix, format }) // Check for legacy ESLint configuration and show deprecation warning const config = getConfig() const legacyConfigFiles = detectLegacyEslintConfig() + // @ts-expect-error - legacy config warning is not in Config types yet if (legacyConfigFiles.length > 0 && config.eslintLegacyConfigWarning) { showLegacyEslintDeprecationWarning(legacyConfigFiles) } try { - const pathString = path?.join(' ') + const pathString = filePath?.join(' ') const sbPath = getPaths().web.storybook const args = [ 'eslint', @@ -130,7 +138,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 +146,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 69% rename from packages/cli/src/commands/prerenderHandler.js rename to packages/cli/src/commands/prerenderHandler.ts index 8101c89abb..3425d0e98c 100644 --- a/packages/cli/src/commands/prerenderHandler.js +++ b/packages/cli/src/commands/prerenderHandler.ts @@ -7,13 +7,16 @@ import { recordTelemetryAttributes } from '@cedarjs/cli-helpers' import { getConfig, getPaths, projectIsEsm } from '@cedarjs/project-config' 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 { 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 +24,7 @@ const mapRouterPathToHtml = (routerPath) => { } } -function getRouteHooksFilePath(routeFilePath) { +function getRouteHooksFilePath(routeFilePath: string) { const routeHooksFilePathTs = routeFilePath.replace( /\.[jt]sx?$/, '.routeHooks.ts', @@ -43,38 +46,23 @@ function getRouteHooksFilePath(routeFilePath) { return undefined } +interface Route { + name: string + path: string + routePath: string + hasParams: boolean + id: string + isNotFound: boolean + filePath: string +} + /** * Takes a route with a path like /blog-post/{id:Int} * Reads path parameters from BlogPostPage.routeHooks.js and returns a list of * routes with the path parameter placeholders (like {id:Int}) replaced by * actual values - * - * So for values like [{ id: 1 }, { id: 2 }, { id: 3 }] (and, again, a route - * path like /blog-post/{id:Int}) it will return three routes with the paths - * /blog-post/1 - * /blog-post/2 - * /blog-post/3 - * - * The paths will be strings. Parsing those path parameters to the correct - * datatype according to the type notation ("Int" in the example above) will - * be handled by the normal router functions, just like when rendering in a - * client browser - * - * Example `route` parameter - * { - * name: 'blogPost', - * path: '/blog-post/{id:Int}', - * routePath: '/blog-post/{id:Int}', - * hasParams: true, - * id: 'file:///Users/tobbe/tmp/rw-prerender-cell-ts/web/src/Routes.tsx 1959', - * isNotFound: false, - * filePath: '/Users/tobbe/tmp/rw-prerender-cell-ts/web/src/pages/BlogPostPage/BlogPostPage.tsx' - * } - * - * 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: Route): Promise { const routeHooksFilePath = getRouteHooksFilePath(route.filePath) if (!routeHooksFilePath) { @@ -94,21 +82,22 @@ async function expandRouteParameters(route) { }) if (routeParameters) { - return routeParameters.map((pathParamValues) => { + return (routeParameters as Record[]).map((pathParamValues) => { let newPath = route.path - Object.entries(pathParamValues).forEach(([paramName, paramValue]) => { + Object.entries(pathParamValues).forEach(([_paramName, paramValue]) => { newPath = newPath.replace( - new RegExp(`{${paramName}:?[^}]*}`), - paramValue, + new RegExp(`{\${_paramName}:?[^}]*}`), + String(paramValue), ) }) return { ...route, path: newPath } }) } - } catch (e) { - console.error(c.error(e.stack)) + } catch (e: unknown) { + const stack = e instanceof Error ? e.stack : String(e) + console.error(c.error(stack)) return [route] } @@ -116,14 +105,15 @@ async function expandRouteParameters(route) { } // This is used directly in build.js for nested ListrTasks -export const getTasks = async (dryrun, routerPathFilter = null) => { - const detector = projectIsEsm() +export const getTasks = async (dryrun: boolean, routerPathFilter: string | null = null) => { + const detector = (projectIsEsm() ? await import('@cedarjs/prerender/detection') - : await import('@cedarjs/prerender/cjs/detection') + : await import('@cedarjs/prerender/cjs/detection')) as Record - const prerenderRoutes = detector + const prerenderRoutes = (detector as any /* @cedarjs/prerender is not perfectly typed here */) .detectPrerenderRoutes() - .filter((route) => route.path) + .filter((route: Route) => route.path) as Route[] + const indexHtmlPath = path.join(getPaths().web.dist, 'index.html') if (prerenderRoutes.length === 0) { console.log('\nSkipping prerender...') @@ -142,7 +132,6 @@ export const getTasks = async (dryrun, routerPathFilter = null) => { 'You must run `yarn cedar build web` before trying to prerender.', ) process.exit(1) - // TODO: Run this automatically at this point. } configureBabel() @@ -159,36 +148,25 @@ export const getTasks = async (dryrun, routerPathFilter = null) => { // queryCache will be filled with the queries from all the Cells we // encounter while prerendering, and the result from executing those // queries. - // We have this cache here because we can potentially reuse result data - // between different pages. I.e. if the same query, with the same - // variables is encountered twice, we'd only have to execute it once and - // then just reuse the cached result the second time. const queryCache = {} - // In principle you could be prerendering a large number of routes, and - // when this occurs not only can it break but it's also not particularly - // useful to enumerate all the routes in the output. + // In principle you could be prerendering a large number of routes const shouldFold = routesToPrerender.length > 16 if (shouldFold) { - // If we're folding the output, we don't need to return the individual - // routes, just a top level message indicating the route and the progress const displayIncrement = Math.max( 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: any /* ListrTaskWrapper is hard to type here */) => { for (let i = 0; i < routesToPrerender.length; i++) { const routeToPrerender = routesToPrerender[i] - // Filter out routes that don't match the supplied routePathFilter if ( routerPathFilter && routeToPrerender.path !== routerPathFilter @@ -214,10 +192,7 @@ export const getTasks = async (dryrun, routerPathFilter = null) => { ] } - // If we're not folding the output, we'll return a list of tasks for each - // individual case. return routesToPrerender.map((routeToPrerender) => { - // Filter out routes that don't match the supplied routePathFilter if (routerPathFilter && routeToPrerender.path !== routerPathFilter) { return [] } @@ -235,7 +210,7 @@ export const getTasks = async (dryrun, routerPathFilter = null) => { ) }, } - }) + }).flat() }) return listrTasks @@ -270,7 +245,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)) @@ -295,7 +270,6 @@ const diagnosticCheck = () => { console.log() - // Exit, no need to show other messages process.exit(1) } else { console.log('✔ Diagnostics checks passed \n') @@ -303,14 +277,13 @@ const diagnosticCheck = () => { } const prerenderRoute = async ( - prerenderer, - queryCache, - routeToPrerender, - dryrun, - outputHtmlPath, + prerenderer: any /* @cedarjs/prerender is not perfectly typed here */, + queryCache: Record, + routeToPrerender: Route, + 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}`, ) @@ -325,7 +298,9 @@ const prerenderRoute = async ( if (!dryrun) { 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 +308,25 @@ 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,20 +343,19 @@ 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() + // eslint-disable-next-line @typescript-eslint/await-thenable + await new Listr(listrTasks, { + renderer: verbose ? 'verbose' : 'default', + rendererOptions: { collapseSubtasks: false }, + concurrent: false, + }).run() } catch (e) { console.log() await diagnosticCheck() @@ -393,7 +371,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 68% rename from packages/cli/src/commands/prismaHandler.js rename to packages/cli/src/commands/prismaHandler.ts index 7f82234601..36afb7d130 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,12 +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 }) => { - recordTelemetryAttributes({ +interface PrismaOptions { + _?: string[] + $0?: string + commands?: string[] + [key: string]: unknown +} + +export const handler = async ({ + _ = [], + $0: _0 = '', + commands = [], + ...options +}: PrismaOptions) => { recordTelemetryAttributes({ command: 'prisma', }) @@ -40,16 +52,17 @@ 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) - } else if (typeof value === 'number') { - args.push(value) + if (typeof value === 'string') { + if (value.split(' ').length > 1) { + args.push(`"${value}"`) + } else { + args.push(value) + } + } else if (typeof value === 'number') { args.push(value) } } @@ -60,7 +73,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 +82,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 78% rename from packages/cli/src/commands/test.js rename to packages/cli/src/commands/test.ts index 0e0c95c674..10138f7ce9 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) => { 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 61% rename from packages/cli/src/commands/type-checkHandler.js rename to packages/cli/src/commands/type-checkHandler.ts index 2e16175e8e..a1fca86e51 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,22 @@ 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 +93,8 @@ export const handler = async ({ sides, verbose, prisma, generate }) => { }, ], { - renderer: verbose && 'verbose', + // @ts-expect-error - Listr renderer type issue + 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 85% rename from packages/cli/src/commands/upgrade.js rename to packages/cli/src/commands/upgrade.ts index 2a4d43de68..1bbd9807ee 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,7 +111,35 @@ 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, @@ -145,10 +178,11 @@ export const handler = async ({ dryRun, tag, verbose, dedupe, yes, force }) => { message: 'This will upgrade your CedarJS project to the latest version. Do you want to proceed?', initial: 'Y', + // @ts-expect-error - default is not in PromptOptions but Enquirer supports it default: '(Yes/no)', - format: function (value) { + format: function (this: any /* Enquirer state is not easily typed here */, value: unknown) { if (this.state.submitted) { - return this.isTrue(value) ? 'yes' : 'no' + return this.isTrue(value) ? 'no' : 'yes' } return 'Yes' @@ -234,7 +268,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 +333,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 +359,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 +377,7 @@ 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 +386,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 +399,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 +451,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') } @@ -430,7 +483,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') } @@ -465,7 +521,7 @@ async function updatePackageVersionsFromTemplate(ctx, { dryRun, verbose }) { const localPackageJson = JSON.parse(localPackageJsonText) 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) { @@ -480,7 +536,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) { @@ -507,7 +563,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') } @@ -524,11 +583,11 @@ async function downloadYarnPatches(ctx, { dryRun, verbose }) { Authorization: githubToken ? `Bearer ${githubToken}` : undefined, ['X-GitHub-Api-Version']: '2022-11-28', Accept: 'application/vnd.github+json', - }, + } as HeadersInit, }, ) - const json = await res.json() + const json = (await res.json()) as { tree?: { path: string; url: string }[] } const patches = json.tree?.filter((patchInfo) => patchInfo.path.startsWith( 'packages/create-cedar-app/templates/ts/.yarn/patches/', @@ -551,7 +610,7 @@ async function downloadYarnPatches(ctx, { dryRun, verbose }) { title: `Downloading ${patch.path}`, task: async () => { const res = await fetch(patch.url) - const patchMeta = await res.json() + const patchMeta = (await res.json()) as { content: string } const patchPath = path.join( getPaths().base, '.yarn', @@ -572,7 +631,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 { @@ -580,23 +642,24 @@ 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, stdio: verbose ? 'inherit' : 'pipe', cwd: getPaths().base, }) - } catch (e) { + } catch (e: any) { console.log(c.error(e.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', @@ -606,7 +669,11 @@ const dedupeDeps = async (_task, { verbose }) => { } // exported for testing -export async function runPreUpgradeScripts(ctx, task, { verbose, force }) { +export async function runPreUpgradeScripts( + ctx: Record, + task: any, // ListrTaskWrapper is complex to type here without importing many things + { verbose, force }: { verbose?: boolean; force?: boolean }, +) { if (!ctx.versionToUpgradeTo) { return } @@ -617,12 +684,12 @@ export async function runPreUpgradeScripts(ctx, task, { verbose, force }) { '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) if (res.status === 200) { - manifest = await res.json() + manifest = (await res.json()) as string[] } else { if (verbose) { console.log('No upgrade script manifest found.') @@ -638,7 +705,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({ @@ -671,7 +738,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) { @@ -729,7 +796,11 @@ export async function runPreUpgradeScripts(ctx, task, { verbose, force }) { ) } - const files = await dirRes.json() + const files = (await dirRes.json()) as { + type: string + name: string + download_url: string + }[] // Download all files in the directory for (const file of files) { @@ -808,7 +879,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 }, ) @@ -819,19 +890,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}` @@ -851,7 +930,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 @@ -896,5 +975,5 @@ const extractDependencies = (content) => { } } - return Array.from(deps.values()) + return Array.from(deps.values()) as string[] } From 578a8eaa77fc5e6d8e1b18ee412c74ff154648df Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Sun, 28 Dec 2025 23:59:01 +0100 Subject: [PATCH 2/8] format --- .../cli/src/commands/__tests__/build.test.ts | 8 +- .../src/commands/__tests__/type-check.test.ts | 6 +- .../src/commands/__tests__/upgrade.test.ts | 4 +- packages/cli/src/commands/buildHandler.ts | 4 +- packages/cli/src/commands/consoleHandler.ts | 7 +- packages/cli/src/commands/execHandler.ts | 2 +- packages/cli/src/commands/prerenderHandler.ts | 96 +++++++++++-------- packages/cli/src/commands/prismaHandler.ts | 18 ++-- packages/cli/src/commands/test.ts | 1 - .../cli/src/commands/type-checkHandler.ts | 4 +- packages/cli/src/commands/upgrade.ts | 15 ++- 11 files changed, 106 insertions(+), 59 deletions(-) diff --git a/packages/cli/src/commands/__tests__/build.test.ts b/packages/cli/src/commands/__tests__/build.test.ts index a7c8388a9f..b1dc0a99e1 100644 --- a/packages/cli/src/commands/__tests__/build.test.ts +++ b/packages/cli/src/commands/__tests__/build.test.ts @@ -68,7 +68,9 @@ afterEach(() => { test('the build tasks are in the correct sequence', async () => { await handler({}) // @ts-expect-error - Listr is mocked - expect(vi.mocked(Listr).mock.calls[0][0].map((x: { title: string }) => x.title)).toMatchInlineSnapshot(` + expect( + vi.mocked(Listr).mock.calls[0][0].map((x: { title: string }) => x.title), + ).toMatchInlineSnapshot(` [ "Generating Prisma Client...", "Verifying graphql schema...", @@ -87,7 +89,9 @@ test('Should run prerender for web', async () => { await handler({ side: ['web'], prerender: true }) // @ts-expect-error - Listr is mocked - expect(vi.mocked(Listr).mock.calls[0][0].map((x: { title: string }) => x.title)).toMatchInlineSnapshot(` + expect( + vi.mocked(Listr).mock.calls[0][0].map((x: { title: string }) => x.title), + ).toMatchInlineSnapshot(` [ "Building Web...", ] diff --git a/packages/cli/src/commands/__tests__/type-check.test.ts b/packages/cli/src/commands/__tests__/type-check.test.ts index 1a2676d7b3..2d66a322cb 100644 --- a/packages/cli/src/commands/__tests__/type-check.test.ts +++ b/packages/cli/src/commands/__tests__/type-check.test.ts @@ -35,7 +35,10 @@ vi.mock('../../lib', async (importOriginal) => { return { ...originalLib, runCommandTask: vi.fn((commands) => { - return commands.map(({ cmd, args }: { cmd: string; args?: string[] }) => `${cmd} ${args?.join(' ')}`) + return commands.map( + ({ cmd, args }: { cmd: string; args?: string[] }) => + `${cmd} ${args?.join(' ')}`, + ) }), getPaths: () => ({ base: './myBasePath', @@ -113,4 +116,3 @@ test('Should generate prisma client', async () => { /.+(\\|\/)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 7f5d2f4310..3ff08a12f7 100644 --- a/packages/cli/src/commands/__tests__/upgrade.test.ts +++ b/packages/cli/src/commands/__tests__/upgrade.test.ts @@ -139,7 +139,9 @@ describe('runPreUpgradeScripts', () => { ` // Mock readFile to return the script content - vi.mocked(fs.promises.readFile).mockResolvedValue(scriptContent as any /* simplified for test mock */) + vi.mocked(fs.promises.readFile).mockResolvedValue( + scriptContent as any /* simplified for test mock */, + ) vi.mocked(fetch).mockImplementation(async (url: string | URL | Request) => { if (url.toString().endsWith('/manifest.json')) { diff --git a/packages/cli/src/commands/buildHandler.ts b/packages/cli/src/commands/buildHandler.ts index 6b740a8650..08679ce4e2 100644 --- a/packages/cli/src/commands/buildHandler.ts +++ b/packages/cli/src/commands/buildHandler.ts @@ -142,7 +142,9 @@ export const handler = async ({ } }, }, - ].filter((x): x is Exclude => !!x) + ].filter( + (x): x is Exclude => !!x, + ) const triggerPrerender = async () => { console.log('Starting prerendering...') diff --git a/packages/cli/src/commands/consoleHandler.ts b/packages/cli/src/commands/consoleHandler.ts index 041d4b4a88..7eab84cd76 100644 --- a/packages/cli/src/commands/consoleHandler.ts +++ b/packages/cli/src/commands/consoleHandler.ts @@ -41,7 +41,12 @@ const loadConsoleHistory = async (r: repl.REPLServer) => { history .split('\n') .reverse() - .map((line) => (r as any /* history is not in Node REPL types but exists in implementation */).history.push(line)) + .map((line) => + ( + r as any + ) /* history is not in Node REPL types but exists in implementation */.history + .push(line), + ) } catch { // We can ignore this -- it just means the user doesn't have any history yet } diff --git a/packages/cli/src/commands/execHandler.ts b/packages/cli/src/commands/execHandler.ts index bf95eefd8f..606abeaa0b 100644 --- a/packages/cli/src/commands/execHandler.ts +++ b/packages/cli/src/commands/execHandler.ts @@ -191,4 +191,4 @@ function resolveScriptPath(name: string) { } return null -} \ No newline at end of file +} diff --git a/packages/cli/src/commands/prerenderHandler.ts b/packages/cli/src/commands/prerenderHandler.ts index 3425d0e98c..a5cb0e9317 100644 --- a/packages/cli/src/commands/prerenderHandler.ts +++ b/packages/cli/src/commands/prerenderHandler.ts @@ -82,18 +82,22 @@ async function expandRouteParameters(route: Route): Promise { }) if (routeParameters) { - return (routeParameters as Record[]).map((pathParamValues) => { - let newPath = route.path - - Object.entries(pathParamValues).forEach(([_paramName, paramValue]) => { - newPath = newPath.replace( - new RegExp(`{\${_paramName}:?[^}]*}`), - String(paramValue), + return (routeParameters as Record[]).map( + (pathParamValues) => { + let newPath = route.path + + Object.entries(pathParamValues).forEach( + ([_paramName, paramValue]) => { + newPath = newPath.replace( + new RegExp(`{\${_paramName}:?[^}]*}`), + String(paramValue), + ) + }, ) - }) - return { ...route, path: newPath } - }) + return { ...route, path: newPath } + }, + ) } } catch (e: unknown) { const stack = e instanceof Error ? e.stack : String(e) @@ -105,12 +109,19 @@ async function expandRouteParameters(route: Route): Promise { } // This is used directly in build.js for nested ListrTasks -export const getTasks = async (dryrun: boolean, routerPathFilter: string | null = null) => { - const detector = (projectIsEsm() - ? await import('@cedarjs/prerender/detection') - : await import('@cedarjs/prerender/cjs/detection')) as Record - - const prerenderRoutes = (detector as any /* @cedarjs/prerender is not perfectly typed here */) +export const getTasks = async ( + dryrun: boolean, + routerPathFilter: string | null = null, +) => { + const detector = ( + projectIsEsm() + ? await import('@cedarjs/prerender/detection') + : await import('@cedarjs/prerender/cjs/detection') + ) as Record + + const prerenderRoutes = ( + detector as any + ) /* @cedarjs/prerender is not perfectly typed here */ .detectPrerenderRoutes() .filter((route: Route) => route.path) as Route[] @@ -163,7 +174,10 @@ export const getTasks = async (dryrun: boolean, routerPathFilter: string | null return [ { title: title(0), - task: async (_: unknown, task: any /* ListrTaskWrapper is hard to type here */) => { + task: async ( + _: unknown, + task: any /* ListrTaskWrapper is hard to type here */, + ) => { for (let i = 0; i < routesToPrerender.length; i++) { const routeToPrerender = routesToPrerender[i] @@ -192,25 +206,27 @@ export const getTasks = async (dryrun: boolean, routerPathFilter: string | null ] } - return routesToPrerender.map((routeToPrerender) => { - if (routerPathFilter && routeToPrerender.path !== routerPathFilter) { - return [] - } - - const outputHtmlPath = mapRouterPathToHtml(routeToPrerender.path) - return { - title: `Prerendering ${routeToPrerender.path} -> ${outputHtmlPath}`, - task: async () => { - await prerenderRoute( - prerenderer, - queryCache, - routeToPrerender, - dryrun, - outputHtmlPath, - ) - }, - } - }).flat() + return routesToPrerender + .map((routeToPrerender) => { + if (routerPathFilter && routeToPrerender.path !== routerPathFilter) { + return [] + } + + const outputHtmlPath = mapRouterPathToHtml(routeToPrerender.path) + return { + title: `Prerendering ${routeToPrerender.path} -> ${outputHtmlPath}`, + task: async () => { + await prerenderRoute( + prerenderer, + queryCache, + routeToPrerender, + dryrun, + outputHtmlPath, + ) + }, + } + }) + .flat() }) return listrTasks @@ -326,7 +342,11 @@ interface HandlerOptions { verbose?: boolean } -export const handler = async ({ path: routerPath, dryRun = false, verbose = false }: HandlerOptions) => { +export const handler = async ({ + path: routerPath, + dryRun = false, + verbose = false, +}: HandlerOptions) => { if (getConfig().experimental?.streamingSsr?.enabled) { console.log( c.warning( @@ -371,7 +391,7 @@ export const handler = async ({ path: routerPath, dryRun = false, verbose = fals } 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/prismaHandler.ts b/packages/cli/src/commands/prismaHandler.ts index 36afb7d130..81bc1cba77 100644 --- a/packages/cli/src/commands/prismaHandler.ts +++ b/packages/cli/src/commands/prismaHandler.ts @@ -24,7 +24,8 @@ export const handler = async ({ $0: _0 = '', commands = [], ...options -}: PrismaOptions) => { recordTelemetryAttributes({ +}: PrismaOptions) => { + recordTelemetryAttributes({ command: 'prisma', }) @@ -56,13 +57,14 @@ export const handler = async ({ 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') { - if (value.split(' ').length > 1) { - args.push(`"${value}"`) - } else { - args.push(value) - } - } else if (typeof value === 'number') { args.push(value) + if (typeof value === 'string') { + if (value.split(' ').length > 1) { + args.push(`"${value}"`) + } else { + args.push(value) + } + } else if (typeof value === 'number') { + args.push(value) } } diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 10138f7ce9..2e4c70eefc 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -59,4 +59,3 @@ export const handler = async (options: TestOptions) => { const { handler } = await import('./testHandler.js') return handler(options) } - diff --git a/packages/cli/src/commands/type-checkHandler.ts b/packages/cli/src/commands/type-checkHandler.ts index a1fca86e51..9a1e72a8c9 100644 --- a/packages/cli/src/commands/type-checkHandler.ts +++ b/packages/cli/src/commands/type-checkHandler.ts @@ -57,7 +57,9 @@ export const handler = async ({ // Non-null exit codes const exitCodes = err .map((e: unknown) => - e instanceof Object && 'exitCode' in e && typeof e.exitCode === 'number' + e instanceof Object && + 'exitCode' in e && + typeof e.exitCode === 'number' ? e.exitCode : undefined, ) diff --git a/packages/cli/src/commands/upgrade.ts b/packages/cli/src/commands/upgrade.ts index 6d5f1b70f6..b8b382d289 100644 --- a/packages/cli/src/commands/upgrade.ts +++ b/packages/cli/src/commands/upgrade.ts @@ -180,7 +180,10 @@ export const handler = async ({ initial: 'Y', // @ts-expect-error - default is not in PromptOptions but Enquirer supports it default: '(Yes/no)', - format: function (this: any /* Enquirer state is not easily typed here */, value: unknown) { + format: function ( + this: any /* Enquirer state is not easily typed here */, + value: unknown, + ) { if (this.state.submitted) { return this.isTrue(value) ? 'no' : 'yes' } @@ -377,7 +380,10 @@ async function removeCliCache( } } -async function setLatestVersionToContext(ctx: Record, tag?: string) { +async function setLatestVersionToContext( + ctx: Record, + tag?: string, +) { try { const foundVersion = await latestVersion( '@cedarjs/core', @@ -658,7 +664,10 @@ async function refreshPrismaClient( } } -const dedupeDeps = async (_task: unknown, { verbose }: { verbose?: boolean }) => { +const dedupeDeps = async ( + _task: unknown, + { verbose }: { verbose?: boolean }, +) => { try { await execa('yarn dedupe', { shell: true, From 2b242c8224064a0bd4482c3b16b89f4adf1c3c23 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 29 Dec 2025 00:08:21 +0100 Subject: [PATCH 3/8] fix eslint issues --- packages/cli/src/commands/__tests__/type-check.test.ts | 5 +++-- packages/cli/src/commands/prerenderHandler.ts | 3 +-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/__tests__/type-check.test.ts b/packages/cli/src/commands/__tests__/type-check.test.ts index 2d66a322cb..e0027a912a 100644 --- a/packages/cli/src/commands/__tests__/type-check.test.ts +++ b/packages/cli/src/commands/__tests__/type-check.test.ts @@ -3,9 +3,8 @@ import path from 'node:path' import concurrently from 'concurrently' import execa from 'execa' import { vi, beforeEach, afterEach, test, expect } from 'vitest' -import type * as Lib from '../../lib' -import '../../lib/mockTelemetry' +import '../../lib/mockTelemetry.js' vi.mock('execa', () => ({ default: vi.fn((cmd, params, options) => { @@ -53,6 +52,8 @@ vi.mock('../../lib', async (importOriginal) => { } }) +// @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' diff --git a/packages/cli/src/commands/prerenderHandler.ts b/packages/cli/src/commands/prerenderHandler.ts index a5cb0e9317..dc4b2778c5 100644 --- a/packages/cli/src/commands/prerenderHandler.ts +++ b/packages/cli/src/commands/prerenderHandler.ts @@ -370,7 +370,6 @@ export const handler = async ({ console.log(c.info('::: Dry run, not writing changes :::')) } - // eslint-disable-next-line @typescript-eslint/await-thenable await new Listr(listrTasks, { renderer: verbose ? 'verbose' : 'default', rendererOptions: { collapseSubtasks: false }, @@ -378,7 +377,7 @@ export const handler = async ({ }).run() } catch (e) { console.log() - await diagnosticCheck() + diagnosticCheck() console.log(c.warning('Tips:')) From f52c450d7614d30db9a9f5a2ab8fb33cd3c90e1e Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 29 Dec 2025 00:10:42 +0100 Subject: [PATCH 4/8] type tweaks --- packages/cli/src/commands/prerenderHandler.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/commands/prerenderHandler.ts b/packages/cli/src/commands/prerenderHandler.ts index dc4b2778c5..cb7f671f44 100644 --- a/packages/cli/src/commands/prerenderHandler.ts +++ b/packages/cli/src/commands/prerenderHandler.ts @@ -99,7 +99,7 @@ async function expandRouteParameters(route: Route): Promise { }, ) } - } catch (e: unknown) { + } catch (e) { const stack = e instanceof Error ? e.stack : String(e) console.error(c.error(stack)) return [route] @@ -113,15 +113,11 @@ export const getTasks = async ( dryrun: boolean, routerPathFilter: string | null = null, ) => { - const detector = ( - projectIsEsm() - ? await import('@cedarjs/prerender/detection') - : await import('@cedarjs/prerender/cjs/detection') - ) as Record - - const prerenderRoutes = ( - detector as any - ) /* @cedarjs/prerender is not perfectly typed here */ + const detector = projectIsEsm() + ? await import('@cedarjs/prerender/detection') + : await import('@cedarjs/prerender/cjs/detection') + + const prerenderRoutes = detector .detectPrerenderRoutes() .filter((route: Route) => route.path) as Route[] From 2083687632bab2005981682222f63422d5476a09 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 29 Dec 2025 08:57:12 +0100 Subject: [PATCH 5/8] add build:types to cli --- packages/cli/package.json | 1 + packages/cli/src/commands/buildHandler.ts | 1 - packages/cli/src/commands/upgrade.ts | 7 +++---- packages/cli/src/lib/__tests__/getDevNodeOptions.test.ts | 2 +- packages/cli/src/testUtils/index.ts | 2 -- packages/cli/src/testUtils/matchFolderTransform.ts | 2 -- packages/cli/tsconfig.build.json | 2 +- packages/cli/tsconfig.json | 4 +++- 8 files changed, 9 insertions(+), 12 deletions(-) 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/buildHandler.ts b/packages/cli/src/commands/buildHandler.ts index 08679ce4e2..08416a4a14 100644 --- a/packages/cli/src/commands/buildHandler.ts +++ b/packages/cli/src/commands/buildHandler.ts @@ -168,7 +168,6 @@ export const handler = async ({ }) } - // @ts-expect-error - tasks might have incompatible types for Listr const jobs = new Listr(tasks, { // @ts-expect-error - renderer might be incompatible renderer: verbose && 'verbose', diff --git a/packages/cli/src/commands/upgrade.ts b/packages/cli/src/commands/upgrade.ts index b8b382d289..04692daddb 100644 --- a/packages/cli/src/commands/upgrade.ts +++ b/packages/cli/src/commands/upgrade.ts @@ -178,7 +178,6 @@ export const handler = async ({ message: 'This will upgrade your CedarJS project to the latest version. Do you want to proceed?', initial: 'Y', - // @ts-expect-error - default is not in PromptOptions but Enquirer supports it default: '(Yes/no)', format: function ( this: any /* Enquirer state is not easily typed here */, @@ -244,13 +243,13 @@ export const handler = async ({ title: 'Running yarn install', task: (ctx) => yarnInstall(ctx, { 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', @@ -526,7 +525,7 @@ async function updatePackageVersionsFromTemplate( const localPackageJsonText = fs.readFileSync(pkgJsonPath, 'utf-8') const localPackageJson = JSON.parse(localPackageJsonText) - const messages = [] + const messages: string[] = [] Object.entries(templatePackageJson.dependencies || {}).forEach( ([depName, depVersion]: [string, unknown]) => { 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/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__"], From 83d16d105a6f0f2235ece8bcf7758584b9cb31e9 Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 29 Dec 2025 11:19:52 +0100 Subject: [PATCH 6/8] TS fixes --- GEMINI.md | 1 + .../cli/src/commands/__tests__/build.test.ts | 19 ++--- .../cli/src/commands/__tests__/test.test.ts | 75 +++++++------------ .../src/commands/__tests__/upgrade.test.ts | 57 +++++++------- packages/cli/src/commands/buildHandler.ts | 2 +- packages/cli/src/commands/consoleHandler.ts | 27 ++++--- packages/cli/src/commands/devHandler.ts | 4 + packages/cli/src/commands/execHandler.ts | 12 ++- packages/cli/src/commands/lint.ts | 15 +++- packages/cli/src/commands/prerenderHandler.ts | 34 ++++++--- packages/cli/src/commands/test.ts | 1 + .../cli/src/commands/type-checkHandler.ts | 1 - packages/cli/src/commands/upgrade.ts | 62 ++++++++------- packages/cli/src/testLib/cells.ts | 29 +++---- 14 files changed, 174 insertions(+), 165 deletions(-) diff --git a/GEMINI.md b/GEMINI.md index 056fc8520e..5122e38046 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -72,6 +72,7 @@ To test framework changes against a real Cedar project: - **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. 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:** Add code comments sparingly. Focus on _why_ something is done, especially for complex logic, rather than _what_ is done. Place comments on a separate line immediately above the affected code using `//`. 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/src/commands/__tests__/build.test.ts b/packages/cli/src/commands/__tests__/build.test.ts index b1dc0a99e1..6788c858ee 100644 --- a/packages/cli/src/commands/__tests__/build.test.ts +++ b/packages/cli/src/commands/__tests__/build.test.ts @@ -5,6 +5,9 @@ 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() return { @@ -48,10 +51,6 @@ vi.mock('node:fs', async (importOriginal) => { } }) -// @ts-expect-error - Mocking Listr -vi.mock('listr2') - -// Make sure prerender doesn't get triggered vi.mock('execa', () => ({ default: vi.fn((cmd, params) => ({ cmd, @@ -67,10 +66,8 @@ afterEach(() => { test('the build tasks are in the correct sequence', async () => { await handler({}) - // @ts-expect-error - Listr is mocked - expect( - vi.mocked(Listr).mock.calls[0][0].map((x: { title: string }) => 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...", @@ -88,10 +85,8 @@ test('Should run prerender for web', async () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) await handler({ side: ['web'], prerender: true }) - // @ts-expect-error - Listr is mocked - expect( - vi.mocked(Listr).mock.calls[0][0].map((x: { title: string }) => 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__/test.test.ts b/packages/cli/src/commands/__tests__/test.test.ts index 5ed7dc347c..9d1d74ff59 100644 --- a/packages/cli/src/commands/__tests__/test.test.ts +++ b/packages/cli/src/commands/__tests__/test.test.ts @@ -31,8 +31,15 @@ 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({} as any /* simplified for test input */) + 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') @@ -41,12 +48,7 @@ test('Runs tests for all available sides if no filter passed', async () => { }) test('Syncs or creates test database when the flag --db-push is set to true', async () => { - await handler( - { - filter: ['api'], - dbPush: true, - } as any /* simplified for test input */, - ) + 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') @@ -55,23 +57,17 @@ test('Syncs or creates test database when the flag --db-push is set to true', as }) test('Skips test database sync/creation when the flag --db-push is set to false', async () => { - await handler( - { - filter: ['api'], - dbPush: false, - } as any /* simplified for test input */, - ) + 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( - { - filter: ['bazinga'], - } as any /* simplified for test input */, - ) + 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') @@ -81,11 +77,7 @@ test('Runs tests for all available sides if no side filter passed', async () => }) test('Runs tests specified side if even with additional filters', async () => { - await handler( - { - filter: ['web', 'bazinga'], - } as any /* simplified for test input */, - ) + 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') @@ -97,22 +89,17 @@ test('Runs tests specified side if even with additional filters', async () => { }) test('Does not create db when calling test with just web', async () => { - await handler( - { - filter: ['web'], - } as any /* simplified for test input */, - ) + 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( - { - filter: ['web', 'bazinga'], - } as any /* simplified for test input */, - ) + 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') @@ -120,14 +107,13 @@ test('Passes filter param to jest command if passed', async () => { }) test('Passes other flags to jest', async () => { - await handler( - { - u: true, - debug: true, - json: true, - collectCoverage: true, - } as any /* simplified for test input */, - ) + 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') @@ -140,12 +126,7 @@ test('Passes other flags to jest', async () => { }) test('Passes values of other flags to jest', async () => { - await handler( - { - bazinga: false, - hello: 'world', - } as any /* simplified for test input */, - ) + await handler({ ...defaultOptions, bazinga: false, hello: 'world' }) // Second command because api side runs expect(vi.mocked(execa).mock.results[0].value.cmd).toBe('yarn') diff --git a/packages/cli/src/commands/__tests__/upgrade.test.ts b/packages/cli/src/commands/__tests__/upgrade.test.ts index 3ff08a12f7..d9e93a43cc 100644 --- a/packages/cli/src/commands/__tests__/upgrade.test.ts +++ b/packages/cli/src/commands/__tests__/upgrade.test.ts @@ -138,10 +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 /* simplified for test mock */, - ) + 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 any /* simplified for test mock */ - } else if (command === 'node') { - return { - stdout: 'Upgrade check passed', - stderr: '', - } as any /* simplified for test mock */ - } - - 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, diff --git a/packages/cli/src/commands/buildHandler.ts b/packages/cli/src/commands/buildHandler.ts index 08416a4a14..b581de3b86 100644 --- a/packages/cli/src/commands/buildHandler.ts +++ b/packages/cli/src/commands/buildHandler.ts @@ -36,7 +36,7 @@ export const handler = async ({ side: JSON.stringify(side), verbose, prisma, - prerender, + prerender: !!prerender, }) const rwjsPaths = getPaths() diff --git a/packages/cli/src/commands/consoleHandler.ts b/packages/cli/src/commands/consoleHandler.ts index 7eab84cd76..89021fc4b3 100644 --- a/packages/cli/src/commands/consoleHandler.ts +++ b/packages/cli/src/commands/consoleHandler.ts @@ -26,8 +26,14 @@ interface REPLServerWithHistory extends repl.REPLServer { lines: string[] } +function isREPLServerWithHistory( + replServer: repl.REPLServer, +): replServer is REPLServerWithHistory { + return 'history' in replServer && 'lines' in replServer +} + const persistConsoleHistory = (r: repl.REPLServer) => { - const lines = (r as REPLServerWithHistory).lines || [] + const lines = isREPLServerWithHistory(r) ? r.lines : [] fs.appendFileSync( consoleHistoryFile, lines.filter((line: string) => line.trim()).join('\n') + '\n', @@ -38,15 +44,12 @@ const persistConsoleHistory = (r: repl.REPLServer) => { const loadConsoleHistory = async (r: repl.REPLServer) => { try { const history = await fs.promises.readFile(consoleHistoryFile, 'utf8') - history - .split('\n') - .reverse() - .map((line) => - ( - r as any - ) /* history is not in Node REPL types but exists in implementation */.history - .push(line), - ) + 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 } @@ -78,8 +81,8 @@ export const handler = () => { // source: https://github.com/nodejs/node/issues/13209#issuecomment-619526317 const defaultEval = r.eval // @ts-expect-error - overriding eval signature - r.eval = (cmd, context, filename, callback) => { - defaultEval(cmd, context, filename, async (err, result) => { + 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, null) diff --git a/packages/cli/src/commands/devHandler.ts b/packages/cli/src/commands/devHandler.ts index 24ad4c5ac1..f0c312b4cf 100644 --- a/packages/cli/src/commands/devHandler.ts +++ b/packages/cli/src/commands/devHandler.ts @@ -259,6 +259,9 @@ export const handler = async ({ const { result } = concurrently( mappedJobs.filter((job) => job.runWhen()), { + // @ts-expect-error - TS is picking up the wrong `ConcurrentlyOptions` + // type. It's getting it from `concurrently.d.ts`. The correct one is in + // `index.d.ts`. The one in index.d.ts has `prefix` prefix: '{name} |', timestampFormat: 'HH:mm:ss', handleInput: true, @@ -271,6 +274,7 @@ export const handler = async ({ process.argv, `Error concurrently starting sides: ${e.message}`, ) + exitWithError(e) } }) diff --git a/packages/cli/src/commands/execHandler.ts b/packages/cli/src/commands/execHandler.ts index 606abeaa0b..d7964e023d 100644 --- a/packages/cli/src/commands/execHandler.ts +++ b/packages/cli/src/commands/execHandler.ts @@ -60,8 +60,8 @@ interface ExecOptions { 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 @@ -114,12 +114,12 @@ export const handler = async (args: ExecOptions) => { const scriptTasks = [ { title: 'Generating Prisma client', - enabled: () => prisma, + enabled: () => !!prisma, task: () => generatePrismaClient({ force: false, verbose: !args.silent, - silent: args.silent, + silent: !!args.silent, }), }, { @@ -131,7 +131,7 @@ export const handler = async (args: ExecOptions) => { functionName: 'default', args: { args: scriptArgs }, }) - } catch (e: unknown) { + } catch (e) { const message = e instanceof Error ? e.message : String(e) console.error(c.error(`Error in script: ${message}`)) throw e @@ -141,8 +141,6 @@ export const handler = async (args: ExecOptions) => { ] const tasks = new Listr(scriptTasks, { - rendererOptions: { collapseSubtasks: false }, - // @ts-expect-error - renderer might be incompatible renderer: args.silent ? 'silent' : 'verbose', }) diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index eb58ef794d..bbff17c215 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -114,13 +114,20 @@ interface LintOptions { } export const handler = async ({ path: filePath, fix, format }: LintOptions) => { - recordTelemetryAttributes({ command: 'lint', fix, format }) + recordTelemetryAttributes({ + command: 'lint', + fix: !!fix, + format: format ?? 'stylish', + }) - // Check for legacy ESLint configuration and show deprecation warning const config = getConfig() const legacyConfigFiles = detectLegacyEslintConfig() - // @ts-expect-error - legacy config warning is not in Config types yet - if (legacyConfigFiles.length > 0 && config.eslintLegacyConfigWarning) { + if ( + legacyConfigFiles.length > 0 && + config instanceof Object && + 'eslintLegacyConfigWarning' in config && + config.eslintLegacyConfigWarning + ) { showLegacyEslintDeprecationWarning(legacyConfigFiles) } diff --git a/packages/cli/src/commands/prerenderHandler.ts b/packages/cli/src/commands/prerenderHandler.ts index cb7f671f44..b53b51f38d 100644 --- a/packages/cli/src/commands/prerenderHandler.ts +++ b/packages/cli/src/commands/prerenderHandler.ts @@ -46,7 +46,7 @@ function getRouteHooksFilePath(routeFilePath: string) { return undefined } -interface Route { +interface PrerenderRoute { name: string path: string routePath: string @@ -54,6 +54,7 @@ interface Route { id: string isNotFound: boolean filePath: string + pageIdentifier: string | undefined } /** @@ -62,7 +63,7 @@ interface Route { * routes with the path parameter placeholders (like {id:Int}) replaced by * actual values */ -async function expandRouteParameters(route: Route): Promise { +async function expandRouteParameters(route: PrerenderRoute) { const routeHooksFilePath = getRouteHooksFilePath(route.filePath) if (!routeHooksFilePath) { @@ -117,11 +118,19 @@ export const getTasks = async ( ? await import('@cedarjs/prerender/detection') : await import('@cedarjs/prerender/cjs/detection') - const prerenderRoutes = detector - .detectPrerenderRoutes() - .filter((route: Route) => route.path) as Route[] + const detectedRoutes: Partial[] = + detector.detectPrerenderRoutes() - const indexHtmlPath = path.join(getPaths().web.dist, 'index.html') + 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( @@ -134,6 +143,9 @@ export const getTasks = async ( 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.', @@ -170,10 +182,7 @@ export const getTasks = async ( return [ { title: title(0), - task: async ( - _: unknown, - task: any /* ListrTaskWrapper is hard to type here */, - ) => { + task: async (_: unknown, task: { title: string }) => { for (let i = 0; i < routesToPrerender.length; i++) { const routeToPrerender = routesToPrerender[i] @@ -288,10 +297,11 @@ const diagnosticCheck = () => { } } +// @cedarjs/prerender is not perfectly typed here const prerenderRoute = async ( - prerenderer: any /* @cedarjs/prerender is not perfectly typed here */, + prerenderer: any, queryCache: Record, - routeToPrerender: Route, + routeToPrerender: PrerenderRoute, dryrun: boolean, outputHtmlPath: string, ) => { diff --git a/packages/cli/src/commands/test.ts b/packages/cli/src/commands/test.ts index 2e4c70eefc..89d4222ff2 100644 --- a/packages/cli/src/commands/test.ts +++ b/packages/cli/src/commands/test.ts @@ -56,6 +56,7 @@ interface TestOptions { } 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-checkHandler.ts b/packages/cli/src/commands/type-checkHandler.ts index 9a1e72a8c9..de65dd0480 100644 --- a/packages/cli/src/commands/type-checkHandler.ts +++ b/packages/cli/src/commands/type-checkHandler.ts @@ -95,7 +95,6 @@ export const handler = async ({ }, ], { - // @ts-expect-error - Listr renderer type issue renderer: verbose ? 'verbose' : 'default', rendererOptions: { collapseSubtasks: false }, }, diff --git a/packages/cli/src/commands/upgrade.ts b/packages/cli/src/commands/upgrade.ts index 04692daddb..6d98864573 100644 --- a/packages/cli/src/commands/upgrade.ts +++ b/packages/cli/src/commands/upgrade.ts @@ -142,12 +142,12 @@ export const handler = async ({ }: 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 = '' @@ -180,7 +180,9 @@ export const handler = async ({ initial: 'Y', default: '(Yes/no)', format: function ( - this: any /* Enquirer state is not easily typed here */, + // Enquirer state is not easily typed here, and 'this' is used + // to access it. + this: any, value: unknown, ) { if (this.state.submitted) { @@ -241,7 +243,7 @@ export const handler = async ({ }, { title: 'Running yarn install', - task: (ctx) => yarnInstall(ctx, { dryRun, verbose }), + task: () => yarnInstall({ dryRun, verbose }), enabled: (ctx) => !ctx.preUpgradeError, skip: () => !!dryRun, }, @@ -478,7 +480,7 @@ function updateCedarJSDepsForAllSides( task: (_ctx, task) => updatePackageJsonVersion( basePath, - ctx.versionToUpgradeTo, + String(ctx.versionToUpgradeTo), task, options, ), @@ -591,18 +593,19 @@ async function downloadYarnPatches( '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', - } as HeadersInit, + }, }, ) - const json = (await res.json()) as { tree?: { path: string; url: string }[] } - const patches = json.tree?.filter((patchInfo) => - patchInfo.path.startsWith( - 'packages/create-cedar-app/templates/ts/.yarn/patches/', - ), + const json = await res.json() + 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') @@ -621,7 +624,7 @@ async function downloadYarnPatches( title: `Downloading ${patch.path}`, task: async () => { const res = await fetch(patch.url) - const patchMeta = (await res.json()) as { content: string } + const patchMeta = await res.json() const patchPath = path.join( getPaths().base, '.yarn', @@ -673,26 +676,33 @@ const dedupeDeps = async ( stdio: verbose ? 'inherit' : 'pipe', cwd: getPaths().base, }) - } catch (e: any) { - console.log(c.error(e.message)) + } catch (e) { + // 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: Record, - task: any, // ListrTaskWrapper is complex to type here without importing many things + 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/' @@ -703,7 +713,7 @@ export async function runPreUpgradeScripts( const res = await fetch(manifestUrl) if (res.status === 200) { - manifest = (await res.json()) as string[] + manifest = await res.json() } else { if (verbose) { console.log('No upgrade script manifest found.') @@ -810,11 +820,7 @@ export async function runPreUpgradeScripts( ) } - const files = (await dirRes.json()) as { - type: string - name: string - download_url: string - }[] + const files = await dirRes.json() // Download all files in the directory for (const file of files) { @@ -989,5 +995,5 @@ const extractDependencies = (content: string) => { } } - return Array.from(deps.values()) as string[] + return Array.from(deps.values()) } 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 = From 9a0b72a71a086dccce194f83990d59e9225fb9bb Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Mon, 29 Dec 2025 11:34:24 +0100 Subject: [PATCH 7/8] remove unused ts-expect-error --- packages/cli/src/commands/devHandler.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/cli/src/commands/devHandler.ts b/packages/cli/src/commands/devHandler.ts index f0c312b4cf..bc7e49552c 100644 --- a/packages/cli/src/commands/devHandler.ts +++ b/packages/cli/src/commands/devHandler.ts @@ -259,9 +259,6 @@ export const handler = async ({ const { result } = concurrently( mappedJobs.filter((job) => job.runWhen()), { - // @ts-expect-error - TS is picking up the wrong `ConcurrentlyOptions` - // type. It's getting it from `concurrently.d.ts`. The correct one is in - // `index.d.ts`. The one in index.d.ts has `prefix` prefix: '{name} |', timestampFormat: 'HH:mm:ss', handleInput: true, From 3017cc3dda7003344e26d1777f3daa22fde3229c Mon Sep 17 00:00:00 2001 From: Tobbe Lundberg Date: Tue, 30 Dec 2025 13:34:46 +0100 Subject: [PATCH 8/8] Fix ` issues, and bring back comments in prerenderHandler --- GEMINI.md | 2 +- packages/cli/src/commands/execHandler.ts | 2 +- packages/cli/src/commands/prerenderHandler.ts | 123 ++++++++++++------ 3 files changed, 84 insertions(+), 43 deletions(-) diff --git a/GEMINI.md b/GEMINI.md index 5122e38046..41c2f79982 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -72,7 +72,7 @@ To test framework changes against a real Cedar project: - **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. 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:** Add code comments sparingly. Focus on _why_ something is done, especially for complex logic, rather than _what_ is done. Place comments on a separate line immediately above the affected code using `//`. Avoid in-line or end-of-line comments. + - **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/src/commands/execHandler.ts b/packages/cli/src/commands/execHandler.ts index d7964e023d..2b6f87ae44 100644 --- a/packages/cli/src/commands/execHandler.ts +++ b/packages/cli/src/commands/execHandler.ts @@ -104,7 +104,7 @@ export const handler = async (args: ExecOptions) => { if (!scriptPath) { console.error( - c.error(`\nNo script called \`\${name}\` in the ./scripts folder.\n`), + c.error(`\nNo script called \`${name}\` in the ./scripts folder.\n`), ) printAvailableScriptsToConsole() diff --git a/packages/cli/src/commands/prerenderHandler.ts b/packages/cli/src/commands/prerenderHandler.ts index b53b51f38d..56fad9b3fb 100644 --- a/packages/cli/src/commands/prerenderHandler.ts +++ b/packages/cli/src/commands/prerenderHandler.ts @@ -4,8 +4,10 @@ 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' @@ -62,6 +64,31 @@ interface PrerenderRoute { * Reads path parameters from BlogPostPage.routeHooks.js and returns a list of * routes with the path parameter placeholders (like {id:Int}) replaced by * actual values + * + * So for values like [{ id: 1 }, { id: 2 }, { id: 3 }] (and, again, a route + * path like /blog-post/{id:Int}) it will return three routes with the paths + * /blog-post/1 + * /blog-post/2 + * /blog-post/3 + * + * The paths will be strings. Parsing those path parameters to the correct + * datatype according to the type notation ("Int" in the example above) will + * be handled by the normal router functions, just like when rendering in a + * client browser + * + * Example `route` parameter + * { + * name: 'blogPost', + * path: '/blog-post/{id:Int}', + * routePath: '/blog-post/{id:Int}', + * hasParams: true, + * id: 'file:///Users/tobbe/tmp/rw-prerender-cell-ts/web/src/Routes.tsx 1959', + * isNotFound: false, + * filePath: '/Users/tobbe/tmp/rw-prerender-cell-ts/web/src/pages/BlogPostPage/BlogPostPage.tsx' + * } + * + * 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: PrerenderRoute) { const routeHooksFilePath = getRouteHooksFilePath(route.filePath) @@ -71,7 +98,7 @@ async function expandRouteParameters(route: PrerenderRoute) { } try { - const routeParameters = await runScriptFunction({ + const routeParameters: Record[] = await runScriptFunction({ path: routeHooksFilePath, functionName: 'routeParameters', args: { @@ -83,22 +110,18 @@ async function expandRouteParameters(route: PrerenderRoute) { }) if (routeParameters) { - return (routeParameters as Record[]).map( - (pathParamValues) => { - let newPath = route.path - - Object.entries(pathParamValues).forEach( - ([_paramName, paramValue]) => { - newPath = newPath.replace( - new RegExp(`{\${_paramName}:?[^}]*}`), - String(paramValue), - ) - }, + return routeParameters.map((pathParamValues) => { + let newPath = route.path + + Object.entries(pathParamValues).forEach(([paramName, paramValue]) => { + newPath = newPath.replace( + new RegExp(`{${paramName}:?[^}]*}`), + String(paramValue), ) + }) - return { ...route, path: newPath } - }, - ) + return { ...route, path: newPath } + }) } } catch (e) { const stack = e instanceof Error ? e.stack : String(e) @@ -167,12 +190,20 @@ export const getTasks = async ( // queryCache will be filled with the queries from all the Cells we // encounter while prerendering, and the result from executing those // queries. + // We have this cache here because we can potentially reuse result data + // between different pages. I.e. if the same query, with the same + // variables is encountered twice, we'd only have to execute it once and + // then just reuse the cached result the second time. const queryCache = {} - // In principle you could be prerendering a large number of routes + // In principle you could be prerendering a large number of routes, and + // when this occurs not only can it break but it's also not particularly + // useful to enumerate all the routes in the output. const shouldFold = routesToPrerender.length > 16 if (shouldFold) { + // If we're folding the output, we don't need to return the individual + // routes, just a top level message indicating the route and the progress const displayIncrement = Math.max( 1, Math.floor(routesToPrerender.length / 100), @@ -183,9 +214,13 @@ export const getTasks = async ( { title: title(0), 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] + // Filter out routes that don't match the supplied routePathFilter if ( routerPathFilter && routeToPrerender.path !== routerPathFilter @@ -211,27 +246,33 @@ export const getTasks = async ( ] } - return routesToPrerender - .map((routeToPrerender) => { - if (routerPathFilter && routeToPrerender.path !== routerPathFilter) { - return [] - } - - const outputHtmlPath = mapRouterPathToHtml(routeToPrerender.path) - return { - title: `Prerendering ${routeToPrerender.path} -> ${outputHtmlPath}`, - task: async () => { - await prerenderRoute( - prerenderer, - queryCache, - routeToPrerender, - dryrun, - outputHtmlPath, - ) - }, - } - }) - .flat() + // If we're not folding the output, we'll return a list of tasks for each + // individual case. + return routesToPrerender.map((routeToPrerender) => { + // Filter out routes that don't match the supplied routePathFilter + if (routerPathFilter && routeToPrerender.path !== routerPathFilter) { + // 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) + return { + title: `Prerendering ${routeToPrerender.path} -> ${outputHtmlPath}`, + task: async () => { + await prerenderRoute( + prerenderer, + queryCache, + routeToPrerender, + dryrun, + outputHtmlPath, + ) + }, + } + }) }) return listrTasks @@ -291,16 +332,16 @@ const diagnosticCheck = () => { console.log() + // Exit, no need to show other messages process.exit(1) } else { console.log('✔ Diagnostics checks passed \n') } } -// @cedarjs/prerender is not perfectly typed here const prerenderRoute = async ( - prerenderer: any, - queryCache: Record, + prerenderer: typeof Prerender, + queryCache: Record, routeToPrerender: PrerenderRoute, dryrun: boolean, outputHtmlPath: string, @@ -317,7 +358,7 @@ const prerenderRoute = async ( renderPath: routeToPrerender.path, }) - if (!dryrun) { + if (!dryrun && typeof prerenderedHtml === 'string') { prerenderer.writePrerenderedHtmlFile(outputHtmlPath, prerenderedHtml) } } catch (e: unknown) {