From 09fe21a6eca50d07ee496f03ab1b4db0060c85b2 Mon Sep 17 00:00:00 2001 From: Wojtek Majewski Date: Mon, 6 Oct 2025 17:47:23 +0200 Subject: [PATCH] fix: typecheck tests using strict tsc mode --- pkgs/client/project.json | 8 +- pkgs/core/project.json | 4 +- pkgs/dsl/__tests__/types/map-method.test-d.ts | 58 +------ .../types/map-return-type-inference.test-d.ts | 164 ++++++++++++++++++ pkgs/dsl/project.json | 8 +- pkgs/dsl/src/dsl.ts | 35 +++- pkgs/edge-worker/project.json | 19 +- pkgs/edge-worker/src/EdgeWorker.ts | 9 +- .../types/flow-compatibility.test-d.ts | 155 ----------------- ...ow-example.ts => supabase-flow.example.ts} | 0 .../src/types/flowCompatibility.ts | 21 --- pkgs/edge-worker/src/types/index.ts | 1 - .../types/compatible-flow.test-d.ts} | 2 +- scripts/typecheck-strict.sh | 102 +++++++++++ 14 files changed, 330 insertions(+), 256 deletions(-) create mode 100644 pkgs/dsl/__tests__/types/map-return-type-inference.test-d.ts delete mode 100644 pkgs/edge-worker/src/__tests__/types/flow-compatibility.test-d.ts rename pkgs/edge-worker/src/examples/{supabase-flow-example.ts => supabase-flow.example.ts} (100%) delete mode 100644 pkgs/edge-worker/src/types/flowCompatibility.ts rename pkgs/edge-worker/{src/examples/type-check-example.ts => tests/types/compatible-flow.test-d.ts} (97%) create mode 100755 scripts/typecheck-strict.sh diff --git a/pkgs/client/project.json b/pkgs/client/project.json index 32e27ec55..d66c83175 100644 --- a/pkgs/client/project.json +++ b/pkgs/client/project.json @@ -159,7 +159,7 @@ "parallel": false } }, - "test": { + "test:vitest": { "executor": "nx:run-commands", "local": true, "dependsOn": ["db:ensure", "build"], @@ -169,6 +169,10 @@ "parallel": false } }, + "test": { + "executor": "nx:noop", + "dependsOn": ["test:vitest", "test:types"] + }, "benchmark": { "executor": "nx:run-commands", "local": true, @@ -183,7 +187,7 @@ "executor": "nx:run-commands", "options": { "cwd": "{projectRoot}", - "command": "tsc --project tsconfig.typecheck.json --noEmit" + "command": "bash ../../scripts/typecheck-strict.sh" } } } diff --git a/pkgs/core/project.json b/pkgs/core/project.json index e1ea14a9c..ff3b0d3c8 100644 --- a/pkgs/core/project.json +++ b/pkgs/core/project.json @@ -194,7 +194,7 @@ }, "test": { "executor": "nx:noop", - "dependsOn": ["test:pgtap", "test:vitest"] + "dependsOn": ["test:pgtap", "test:vitest", "test:types"] }, "test:pgtap": { "executor": "nx:run-commands", @@ -267,7 +267,7 @@ "executor": "nx:run-commands", "options": { "cwd": "{projectRoot}", - "command": "tsc --project tsconfig.typecheck.json --noEmit" + "command": "bash ../../scripts/typecheck-strict.sh" } } } diff --git a/pkgs/dsl/__tests__/types/map-method.test-d.ts b/pkgs/dsl/__tests__/types/map-method.test-d.ts index 25d839bf7..f1c678918 100644 --- a/pkgs/dsl/__tests__/types/map-method.test-d.ts +++ b/pkgs/dsl/__tests__/types/map-method.test-d.ts @@ -18,12 +18,12 @@ describe('.map() method type constraints', () => { }); it('should reject root map when flow input is not array', () => { - // @ts-expect-error - Flow input must be array for root map new Flow({ slug: 'test' }) + // @ts-expect-error - Flow input must be array for root map .map({ slug: 'fail' }, (item) => item); - // @ts-expect-error - Object is not an array new Flow<{ name: string }>({ slug: 'test' }) + // @ts-expect-error - Object is not an array .map({ slug: 'fail2' }, (item) => item); }); @@ -168,53 +168,22 @@ describe('.map() method type constraints', () => { : never; expectTypeOf().toEqualTypeOf(); }); - - it('should allow array step to provide input for map', () => { - const flow = new Flow>({ slug: 'test' }) - .array({ slug: 'generate' }, () => ['a', 'b', 'c']) - .map({ slug: 'process', array: 'generate' }, (letter) => { - expectTypeOf(letter).toEqualTypeOf(); - return { letter, index: letter.charCodeAt(0) }; - }); - - type ProcessOutput = typeof flow extends Flow - ? Steps['process'] - : never; - expectTypeOf().toEqualTypeOf<{ letter: string; index: number }[]>(); - }); }); describe('context inference', () => { it('should preserve context through map methods', () => { const flow = new Flow({ slug: 'test' }) - .map({ slug: 'process' }, (item, context: { api: { transform: (s: string) => string } }) => { - expectTypeOf(context.api.transform).toEqualTypeOf<(s: string) => string>(); + .map({ slug: 'process' }, (item, context) => { + // Let TypeScript infer the full context type expectTypeOf(context.env).toEqualTypeOf>(); expectTypeOf(context.shutdownSignal).toEqualTypeOf(); - return context.api.transform(item); + return String(item); }); type FlowContext = ExtractFlowContext; expectTypeOf().toMatchTypeOf<{ env: Record; shutdownSignal: AbortSignal; - api: { transform: (s: string) => string }; - }>(); - }); - - it('should accumulate context across map and regular steps', () => { - const flow = new Flow({ slug: 'test' }) - .map({ slug: 'transform' }, (n, context: { multiplier: number }) => n * context.multiplier) - .step({ slug: 'aggregate' }, (input, context: { formatter: (n: number) => string }) => - context.formatter(input.transform.reduce((a, b) => a + b, 0)) - ); - - type FlowContext = ExtractFlowContext; - expectTypeOf().toMatchTypeOf<{ - env: Record; - shutdownSignal: AbortSignal; - multiplier: number; - formatter: (n: number) => string; }>(); }); }); @@ -252,25 +221,16 @@ describe('.map() method type constraints', () => { expectTypeOf(squareStep.handler).toBeFunction(); const sumStep = flow.getStepDefinition('sum'); - expectTypeOf(sumStep.handler).parameters.toMatchTypeOf<[{ + // Handler should be typed to receive input and context + expectTypeOf(sumStep.handler).toBeFunction(); + expectTypeOf(sumStep.handler).parameter(0).toEqualTypeOf<{ run: number[]; square: number[]; - }]>(); + }>(); }); }); describe('edge cases', () => { - it('should handle empty arrays', () => { - const flow = new Flow({ slug: 'test' }) - .map({ slug: 'process' }, (item) => ({ processed: item })); - - // Should be able to handle empty array input - type ProcessOutput = typeof flow extends Flow - ? Steps['process'] - : never; - expectTypeOf().toEqualTypeOf<{ processed: Json }[]>(); - }); - it('should handle union types in arrays', () => { const flow = new Flow<(string | number)[]>({ slug: 'test' }) .map({ slug: 'stringify' }, (item) => { diff --git a/pkgs/dsl/__tests__/types/map-return-type-inference.test-d.ts b/pkgs/dsl/__tests__/types/map-return-type-inference.test-d.ts new file mode 100644 index 000000000..7b41277d5 --- /dev/null +++ b/pkgs/dsl/__tests__/types/map-return-type-inference.test-d.ts @@ -0,0 +1,164 @@ +import { Flow } from '../../src/index.js'; +import { describe, it, expectTypeOf } from 'vitest'; + +describe('map step return type inference bug', () => { + it('should preserve specific return type from map handler, not collapse to any[]', () => { + const flow = new Flow<{ items: string[] }>({ slug: 'test' }) + .array({ slug: 'chunks' }, async ({ run }) => { + return [{ data: 'chunk1' }, { data: 'chunk2' }]; + }) + .map( + { slug: 'processChunks', array: 'chunks' }, + async (chunk) => { + return { + chunkIndex: 0, + successes: ['success1'], + errors: [{ line: 1, error: 'test error' }], // Non-empty array for inference + }; + } + ) + .step( + { slug: 'aggregate', dependsOn: ['processChunks'] }, + async ({ run, processChunks }) => { + // Verify types are inferred correctly + expectTypeOf(processChunks).not.toEqualTypeOf(); + + // These should all have proper types, not any + for (const result of processChunks) { + expectTypeOf(result.chunkIndex).toEqualTypeOf(); + expectTypeOf(result.chunkIndex).not.toEqualTypeOf(); + expectTypeOf(result.successes).toEqualTypeOf(); + expectTypeOf(result.successes).not.toEqualTypeOf(); + expectTypeOf(result.errors).toMatchTypeOf>(); + expectTypeOf(result.errors).not.toEqualTypeOf(); + } + + return { done: true }; + } + ); + + // Verify the map step output type is not any[] + type ProcessChunksOutput = typeof flow extends Flow + ? Steps['processChunks'] + : never; + + expectTypeOf().not.toEqualTypeOf(); + }); + + it('should preserve complex nested types through map', () => { + // Note: optional properties not in the return object are not inferred by TypeScript + type ComplexResult = { + nested: { deep: { value: string } }; + array: number[]; + }; + + const flow = new Flow>({ slug: 'test' }) + .array({ slug: 'items' }, () => [1, 2, 3]) + .map({ slug: 'transform', array: 'items' }, async (item) => { + return { + nested: { deep: { value: 'test' } }, + array: [1, 2, 3] + }; + }) + .step({ slug: 'use', dependsOn: ['transform'] }, ({ transform }) => { + expectTypeOf(transform).toEqualTypeOf(); + expectTypeOf(transform).not.toEqualTypeOf(); + + // Verify nested structure is preserved + expectTypeOf(transform[0].nested.deep.value).toEqualTypeOf(); + expectTypeOf(transform[0].nested.deep.value).not.toEqualTypeOf(); + expectTypeOf(transform[0].array).toEqualTypeOf(); + expectTypeOf(transform[0].array).not.toEqualTypeOf(); + + return { ok: true }; + }); + + type TransformOutput = typeof flow extends Flow + ? Steps['transform'] + : never; + + expectTypeOf().toEqualTypeOf(); + expectTypeOf().not.toEqualTypeOf(); + }); + + it('should preserve union-like return types from map', () => { + // Test that return types with discriminated union pattern are inferred correctly + const flow = new Flow({ slug: 'test' }) + .map({ slug: 'process' }, async (item) => { + // Return explicit objects to help TypeScript inference + const success = { success: true as const, data: 'ok' }; + const failure = { success: false as const, error: 'fail' }; + return Math.random() > 0.5 ? success : failure; + }) + .step({ slug: 'aggregate', dependsOn: ['process'] }, ({ process }) => { + expectTypeOf(process).not.toEqualTypeOf(); + + // Verify the inferred type preserves the shape + const firstResult = process[0]; + expectTypeOf(firstResult.success).toEqualTypeOf(); + + return { done: true }; + }); + + type ProcessOutput = typeof flow extends Flow + ? Steps['process'] + : never; + + expectTypeOf().not.toEqualTypeOf(); + }); + + it('should work with inferred return types (no explicit Promise type)', () => { + const flow = new Flow({ slug: 'test' }) + .map({ slug: 'transform' }, (item) => { + return { value: item.toUpperCase(), length: item.length }; + }) + .step({ slug: 'use', dependsOn: ['transform'] }, ({ transform }) => { + // Should infer { value: string; length: number }[] + expectTypeOf(transform).toEqualTypeOf<{ value: string; length: number }[]>(); + expectTypeOf(transform).not.toEqualTypeOf(); + + for (const result of transform) { + expectTypeOf(result.value).toEqualTypeOf(); + expectTypeOf(result.value).not.toEqualTypeOf(); + expectTypeOf(result.length).toEqualTypeOf(); + expectTypeOf(result.length).not.toEqualTypeOf(); + } + + return { ok: true }; + }); + + type TransformOutput = typeof flow extends Flow + ? Steps['transform'] + : never; + + expectTypeOf().toEqualTypeOf<{ value: string; length: number }[]>(); + expectTypeOf().not.toEqualTypeOf(); + }); + + it('should work with root map (no array dependency)', () => { + const flow = new Flow({ slug: 'test' }) + .map({ slug: 'uppercase' }, (item) => { + return { original: item, transformed: item.toUpperCase() }; + }) + .step({ slug: 'aggregate', dependsOn: ['uppercase'] }, ({ uppercase }) => { + expectTypeOf(uppercase).toEqualTypeOf<{ original: string; transformed: string }[]>(); + expectTypeOf(uppercase).not.toEqualTypeOf(); + + for (const result of uppercase) { + expectTypeOf(result.original).toEqualTypeOf(); + expectTypeOf(result.original).not.toEqualTypeOf(); + expectTypeOf(result.transformed).toEqualTypeOf(); + expectTypeOf(result.transformed).not.toEqualTypeOf(); + } + + return { count: uppercase.length }; + }); + + type UppercaseOutput = typeof flow extends Flow + ? Steps['uppercase'] + : never; + + expectTypeOf().toEqualTypeOf<{ original: string; transformed: string }[]>(); + expectTypeOf().not.toEqualTypeOf(); + }); +}); diff --git a/pkgs/dsl/project.json b/pkgs/dsl/project.json index e385003c0..f93ed7e72 100644 --- a/pkgs/dsl/project.json +++ b/pkgs/dsl/project.json @@ -25,7 +25,7 @@ "parallel": false } }, - "test": { + "test:vitest": { "executor": "@nx/vite:test", "dependsOn": ["build"], "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], @@ -38,8 +38,12 @@ "executor": "nx:run-commands", "options": { "cwd": "{projectRoot}", - "command": "tsc --project tsconfig.typecheck.json --noEmit" + "command": "bash ../../scripts/typecheck-strict.sh" } + }, + "test": { + "executor": "nx:noop", + "dependsOn": ["test:vitest", "test:types"] } } } diff --git a/pkgs/dsl/src/dsl.ts b/pkgs/dsl/src/dsl.ts index e7964f6f9..5bd8ecd12 100644 --- a/pkgs/dsl/src/dsl.ts +++ b/pkgs/dsl/src/dsl.ts @@ -178,6 +178,37 @@ export type ExtractFlowContext = TFlow extends Flow< ? FlowContext & TC : never; +/** + * Type guard that ensures a flow's context requirements can be satisfied + * by the resources provided by the platform and optional user resources. + * + * A flow is compatible if the provided platform and user resources can satisfy + * all the context requirements declared by the flow. + * + * @template F - The Flow type to check for compatibility + * @template PlatformResources - Resources provided by the execution platform (e.g., Supabase resources) + * @template UserResources - Additional user-provided resources (default: empty) + * + * @example + * ```typescript + * // In a platform worker: + * type SupabaseCompatibleFlow = CompatibleFlow; + * + * // Usage: + * function startWorker(flow: SupabaseCompatibleFlow) { + * // flow is guaranteed to be compatible with Supabase platform + * } + * ``` + */ +export type CompatibleFlow< + F extends AnyFlow, + PlatformResources extends Record, + UserResources extends Record = Record +> = + (FlowContext> & PlatformResources & UserResources) extends ExtractFlowContext + ? F + : never; + /** * Extracts the dependencies type from a Flow * @template TFlow - The Flow type to extract from @@ -528,7 +559,7 @@ export class Flow< ): Flow< TFlowInput, TContext & BaseContext, - Steps & { [K in Slug]: Awaited any)>>[] }, + Steps & { [K in Slug]: AwaitedReturn[] }, StepDependencies & { [K in Slug]: [] } >; @@ -541,7 +572,7 @@ export class Flow< ): Flow< TFlowInput, TContext & BaseContext, - Steps & { [K in Slug]: Awaited any)>>[] }, + Steps & { [K in Slug]: AwaitedReturn[] }, StepDependencies & { [K in Slug]: [TArrayDep] } >; diff --git a/pkgs/edge-worker/project.json b/pkgs/edge-worker/project.json index 1b0bada04..24406caf2 100644 --- a/pkgs/edge-worker/project.json +++ b/pkgs/edge-worker/project.json @@ -165,28 +165,15 @@ } }, "test": { - "dependsOn": ["test:types:all", "test:unit", "test:integration"] + "dependsOn": ["test:types", "test:unit", "test:integration"] }, - "test:types:tsc": { - "executor": "nx:run-commands", - "options": { - "cwd": "{projectRoot}", - "command": "tsc --project tsconfig.typecheck.json --noEmit" - } - }, - "test:types:examples": { + "test:types": { "executor": "nx:run-commands", "dependsOn": ["^build"], "options": { "cwd": "{projectRoot}", - "command": "deno check --config deno.test.json src/examples/*.ts" + "command": "deno check --config deno.test.json src/examples/*.example.ts tests/types/*.test-d.ts" } - }, - "test:types": { - "dependsOn": ["test:types:tsc", "test:types:examples"] - }, - "test:types:all": { - "dependsOn": ["test:types"] } }, "tags": [] diff --git a/pkgs/edge-worker/src/EdgeWorker.ts b/pkgs/edge-worker/src/EdgeWorker.ts index ce9def0db..636175fac 100644 --- a/pkgs/edge-worker/src/EdgeWorker.ts +++ b/pkgs/edge-worker/src/EdgeWorker.ts @@ -10,8 +10,7 @@ import { import { createAdapter } from './platform/createAdapter.js'; import type { PlatformAdapter } from './platform/types.js'; import type { MessageHandlerFn } from './queue/types.js'; -import type { AnyFlow } from '@pgflow/dsl'; -import type { CompatibleFlow } from './types/flowCompatibility.js'; +import type { AnyFlow, CompatibleFlow } from '@pgflow/dsl'; import type { CurrentPlatformResources } from './types/currentPlatform.js'; @@ -66,7 +65,7 @@ export class EdgeWorker { * @param config - Configuration options for the flow worker */ static async start( - flow: CompatibleFlow, + flow: CompatibleFlow, config?: Omit ): Promise>; @@ -92,7 +91,7 @@ export class EdgeWorker { ); } else { return await this.startFlowWorker( - handlerOrFlow as CompatibleFlow, + handlerOrFlow as CompatibleFlow, config ); } @@ -186,7 +185,7 @@ export class EdgeWorker { * ``` */ static async startFlowWorker( - flow: CompatibleFlow, + flow: CompatibleFlow, config: FlowWorkerConfig = {} ): Promise> { this.ensureFirstCall(); diff --git a/pkgs/edge-worker/src/__tests__/types/flow-compatibility.test-d.ts b/pkgs/edge-worker/src/__tests__/types/flow-compatibility.test-d.ts deleted file mode 100644 index 7e03d98a9..000000000 --- a/pkgs/edge-worker/src/__tests__/types/flow-compatibility.test-d.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { describe, it, expectTypeOf } from 'vitest'; -import { Flow } from '@pgflow/dsl/supabase'; -import type { SupabaseResources } from '@pgflow/dsl/supabase'; -import type { Json } from '@pgflow/dsl'; -import type { Sql } from 'postgres'; - -// Extract SupabaseClient type from the same source dsl uses to guarantee type identity -// This prevents version mismatch issues when @supabase/supabase-js resolves to different versions -type SupabaseClientType = SupabaseResources['supabase']; - -describe('Flow Compatibility Type Tests', () => { - it('should create flows with proper step types', () => { - new Flow({ slug: 'test_flow' }) - .step({ slug: 'step1' }, (_input) => { - return { result: 'ok' }; - }); - }); - - it('should provide correct context type in handlers', () => { - new Flow<{ value: number }>({ slug: 'test_flow' }) - .step({ slug: 'step1' }, (input, context) => { - // Context should have base FlowContext properties - expectTypeOf(context.env).toMatchTypeOf>(); - expectTypeOf(context.shutdownSignal).toMatchTypeOf(); - expectTypeOf(context.stepTask).toMatchTypeOf<{ run_id: string; step_slug: string }>(); - expectTypeOf(context.rawMessage).toMatchTypeOf<{ msg_id: number }>(); - - // Context should have Supabase platform resources - expectTypeOf(context.sql).toMatchTypeOf(); - expectTypeOf(context.supabase).toMatchTypeOf(); - - // Input should have run property - expectTypeOf(input.run).toMatchTypeOf<{ value: number }>(); - - return { processed: true }; - }); - }); - - it('should handle dependent steps correctly', () => { - new Flow<{ id: string }>({ slug: 'test_flow' }) - .step({ slug: 'fetch' }, (input) => { - expectTypeOf(input.run).toMatchTypeOf<{ id: string }>(); - return { data: 'fetched' }; - }) - .step({ slug: 'process', dependsOn: ['fetch'] }, (input, context) => { - // Input should have both run and fetch step output - expectTypeOf(input.run).toMatchTypeOf<{ id: string }>(); - expectTypeOf(input.fetch).toMatchTypeOf<{ data: string }>(); - - // Context should have Supabase resources - expectTypeOf(context.sql).toMatchTypeOf(); - expectTypeOf(context.supabase).toMatchTypeOf(); - - return { processed: true }; - }); - }); - - it('should handle custom context correctly', () => { - interface CustomContext extends Record { - redis: { get: (key: string) => Promise }; - } - - new Flow({ slug: 'test_flow' }) - .step({ slug: 'step1' }, (_input, context) => { - // Should have base context - expectTypeOf(context.env).toMatchTypeOf>(); - expectTypeOf(context.shutdownSignal).toMatchTypeOf(); - - // Should have Supabase resources - expectTypeOf(context.sql).toMatchTypeOf(); - expectTypeOf(context.supabase).toMatchTypeOf(); - - // Should have custom context - expectTypeOf(context.redis).toMatchTypeOf<{ get: (key: string) => Promise }>(); - - return { result: 'ok' }; - }); - }); - - it('should infer correct step output types', () => { - const testFlow = new Flow<{ value: number }>({ slug: 'test_flow' }) - .step({ slug: 'double' }, (input) => { - return { doubled: input.run.value * 2 }; - }) - .step({ slug: 'stringify', dependsOn: ['double'] }, (input) => { - return { text: String(input.double.doubled) }; - }); - - // Verify step definition types - const doubleStep = testFlow.getStepDefinition('double'); - type DoubleOutput = ReturnType; - expectTypeOf().toMatchTypeOf<{ doubled: number } | Promise<{ doubled: number }>>(); - - const stringifyStep = testFlow.getStepDefinition('stringify'); - type StringifyInput = Parameters[0]; - expectTypeOf().toMatchTypeOf<{ - run: { value: number }; - double: { doubled: number }; - }>(); - }); - - it('should handle async handlers', () => { - new Flow<{ id: string }>({ slug: 'test_flow' }) - .step({ slug: 'fetch' }, async (input, context) => { - // Can use async operations - await context.sql`SELECT 1`; - expectTypeOf(input.run).toMatchTypeOf<{ id: string }>(); - return { data: 'fetched' }; - }) - .step({ slug: 'process', dependsOn: ['fetch'] }, (input) => { - // Input correctly typed with dependency - expectTypeOf(input.fetch).toMatchTypeOf<{ data: string }>(); - return { processed: true }; - }); - }); - - it('should handle complex dependency chains', () => { - new Flow<{ url: string }>({ slug: 'test_flow' }) - .step({ slug: 'fetch' }, (_input) => { - return { content: 'html' }; - }) - .step({ slug: 'parse', dependsOn: ['fetch'] }, (_input) => { - return { title: 'Title', body: 'Body' }; - }) - .step({ slug: 'analyze', dependsOn: ['parse'] }, (_input) => { - return { sentiment: 0.8 }; - }) - .step({ slug: 'save', dependsOn: ['parse', 'analyze'] }, (input, context) => { - // Should have access to parse and analyze outputs - expectTypeOf(input.run).toMatchTypeOf<{ url: string }>(); - expectTypeOf(input.parse).toMatchTypeOf<{ title: string; body: string }>(); - expectTypeOf(input.analyze).toMatchTypeOf<{ sentiment: number }>(); - - // Should have Supabase resources - expectTypeOf(context.sql).toMatchTypeOf(); - expectTypeOf(context.supabase).toMatchTypeOf(); - - return { saved: true }; - }); - }); - - it('should handle JSON-serializable inputs and outputs', () => { - const testFlow = new Flow<{ data: Json }>({ slug: 'test_flow' }) - .step({ slug: 'process' }, (input): { result: Json } => { - // Input and output must be JSON-serializable - expectTypeOf(input.run.data).toMatchTypeOf(); - return { result: { processed: true } }; - }); - - // Verify the step output is JSON - const stepDef = testFlow.getStepDefinition('process'); - type Output = ReturnType; - expectTypeOf().toMatchTypeOf<{ result: Json } | Promise<{ result: Json }>>(); - }); -}); diff --git a/pkgs/edge-worker/src/examples/supabase-flow-example.ts b/pkgs/edge-worker/src/examples/supabase-flow.example.ts similarity index 100% rename from pkgs/edge-worker/src/examples/supabase-flow-example.ts rename to pkgs/edge-worker/src/examples/supabase-flow.example.ts diff --git a/pkgs/edge-worker/src/types/flowCompatibility.ts b/pkgs/edge-worker/src/types/flowCompatibility.ts deleted file mode 100644 index 302a68263..000000000 --- a/pkgs/edge-worker/src/types/flowCompatibility.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { AnyFlow, ExtractFlowContext, ExtractFlowEnv, FlowContext } from '@pgflow/dsl'; -import type { CurrentPlatformResources } from './currentPlatform.js'; - -/** - * Type guard that ensures a flow's context requirements can be satisfied - * by the resources provided by the platform (and later custom resources) - * - * A flow is compatible if we can provide what the flow needs - * - * For MVP: Only checks against platform resources (no custom resources) - * Future: Will also check against user-provided custom resources - */ -export type CompatibleFlow< - F extends AnyFlow, - UserResources extends Record = Record -> = - // Check if EdgeWorker CAN PROVIDE what the flow needs - // Extract the env type from the flow and use it for FlowContext - (FlowContext> & CurrentPlatformResources & UserResources) extends ExtractFlowContext - ? F - : never; \ No newline at end of file diff --git a/pkgs/edge-worker/src/types/index.ts b/pkgs/edge-worker/src/types/index.ts index 2d8c62024..e56729809 100644 --- a/pkgs/edge-worker/src/types/index.ts +++ b/pkgs/edge-worker/src/types/index.ts @@ -1,2 +1 @@ -export * from './flowCompatibility.js'; export * from './currentPlatform.js'; \ No newline at end of file diff --git a/pkgs/edge-worker/src/examples/type-check-example.ts b/pkgs/edge-worker/tests/types/compatible-flow.test-d.ts similarity index 97% rename from pkgs/edge-worker/src/examples/type-check-example.ts rename to pkgs/edge-worker/tests/types/compatible-flow.test-d.ts index d93fe5478..3aa961fa8 100644 --- a/pkgs/edge-worker/src/examples/type-check-example.ts +++ b/pkgs/edge-worker/tests/types/compatible-flow.test-d.ts @@ -1,5 +1,5 @@ import { Flow as SupabaseFlow } from '@pgflow/dsl/supabase'; -import { EdgeWorker } from '../EdgeWorker.js'; +import { EdgeWorker } from '../../src/EdgeWorker.js'; import type { Json } from '@pgflow/dsl'; // Example 1: Flow using only platform resources - should work diff --git a/scripts/typecheck-strict.sh b/scripts/typecheck-strict.sh new file mode 100755 index 000000000..39efc5592 --- /dev/null +++ b/scripts/typecheck-strict.sh @@ -0,0 +1,102 @@ +#!/bin/bash +# Two-pass TypeScript type checking +# Pass 1: Project-wide type check +# Pass 2: Individual file checks to catch unused @ts-expect-error directives +# +# Usage: +# typecheck-strict.sh # Check all *.test-d.ts files +# typecheck-strict.sh path/to/file.ts # Check specific file +# typecheck-strict.sh path/to/dir/ # Check all *.test-d.ts in directory + +set +e # Don't exit on first error, collect all errors + +EXIT_CODE=0 +ERRORS_FILE=$(mktemp) +TARGET_PATH="${1:-.}" # Use first argument or current directory + +echo "=========================================" +echo "Pass 1: Project-wide type check" +echo "=========================================" +pnpm tsc --project tsconfig.typecheck.json --noEmit 2>&1 | tee -a "$ERRORS_FILE" +if [ ${PIPESTATUS[0]} -ne 0 ]; then + EXIT_CODE=1 + echo "❌ Project-wide type check failed" +else + echo "✓ Project-wide type check passed" +fi +echo "" + +echo "=========================================" +echo "Pass 2: Individual file strict checks" +echo "=========================================" +echo "Checking for unused @ts-expect-error directives..." +echo "" + +FILE_ERRORS=0 + +# Determine which files to check +if [ -f "$TARGET_PATH" ]; then + # Single file provided + echo "Targeting specific file: $TARGET_PATH" + FILES_TO_CHECK="$TARGET_PATH" +elif [ -d "$TARGET_PATH" ]; then + # Directory provided - find all test-d.ts files + echo "Targeting directory: $TARGET_PATH" + FILES_TO_CHECK=$(find "$TARGET_PATH" -name "*.test-d.ts" -type f) +else + echo "Error: '$TARGET_PATH' is not a valid file or directory" + exit 1 +fi + +# Check each file +while IFS= read -r file; do + [ -z "$file" ] && continue # Skip empty lines + echo "Checking: $file" + + # Create temporary tsconfig in current directory that extends the main one + TEMP_CONFIG=".tsconfig.typecheck-strict-$(basename "$file").json" + cat > "$TEMP_CONFIG" <&1) + TSC_EXIT=$? + rm -f "$TEMP_CONFIG" + + if [ $TSC_EXIT -ne 0 ]; then + # Filter out node_modules errors, keep only test file errors + FILTERED=$(echo "$OUTPUT" | grep -v "node_modules") + + if [ -n "$FILTERED" ]; then + echo "$FILTERED" | tee -a "$ERRORS_FILE" + FILE_ERRORS=$((FILE_ERRORS + 1)) + EXIT_CODE=1 + echo "" + fi + fi +done <<< "$FILES_TO_CHECK" + +if [ $FILE_ERRORS -eq 0 ]; then + echo "✓ All individual file checks passed" +else + echo "❌ $FILE_ERRORS file(s) had type errors" +fi +echo "" + +echo "=========================================" +echo "Summary" +echo "=========================================" +if [ $EXIT_CODE -eq 0 ]; then + echo "✅ All type checks passed!" +else + echo "❌ Type checking failed" + echo "" + echo "Errors found:" + cat "$ERRORS_FILE" +fi + +rm -f "$ERRORS_FILE" +exit $EXIT_CODE