Skip to content

Commit 6c96b03

Browse files
committed
feat: add example worker and improve flow scaffolding (#443)
# Improved Example Flow and Worker for Better Onboarding This PR enhances the onboarding experience by replacing the generic example flow with a more intuitive `GreetUser` flow and adding a corresponding Edge Worker. The changes include: - Created a new `greet-user-worker` that automatically gets scaffolded during installation - Replaced `example-flow.ts` with a more descriptive `greet-user.ts` that demonstrates step dependencies - Added a new "Quickstart" guide to help users run the example flow in 5 minutes - Restructured the "Create Flow" guide to focus on explaining the scaffolded example - Updated the installation completion message to direct users to the quickstart The `GreetUser` flow demonstrates core pgflow concepts with two steps: 1. A `fullName` step that combines first and last name 2. A `greeting` step that depends on the first step's output These changes provide a more complete out-of-the-box experience, allowing new users to see pgflow in action immediately after installation.
1 parent 436f10d commit 6c96b03

File tree

11 files changed

+616
-166
lines changed

11 files changed

+616
-166
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2+
import fs from 'fs';
3+
import path from 'path';
4+
import os from 'os';
5+
import { createExampleWorker } from '../../../src/commands/install/create-example-worker';
6+
import { getVersion } from '../../../src/utils/get-version';
7+
8+
describe('createExampleWorker', () => {
9+
let tempDir: string;
10+
let supabasePath: string;
11+
let workerDir: string;
12+
13+
beforeEach(() => {
14+
// Create a temporary directory for testing
15+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pgflow-test-'));
16+
supabasePath = path.join(tempDir, 'supabase');
17+
workerDir = path.join(supabasePath, 'functions', 'greet-user-worker');
18+
});
19+
20+
afterEach(() => {
21+
// Clean up the temporary directory
22+
fs.rmSync(tempDir, { recursive: true, force: true });
23+
});
24+
25+
it('should create both files when none exist', async () => {
26+
const result = await createExampleWorker({
27+
supabasePath,
28+
autoConfirm: true,
29+
});
30+
31+
// Should return true because files were created
32+
expect(result).toBe(true);
33+
34+
// Verify directory was created
35+
expect(fs.existsSync(workerDir)).toBe(true);
36+
37+
// Verify all files exist
38+
const indexPath = path.join(workerDir, 'index.ts');
39+
const denoJsonPath = path.join(workerDir, 'deno.json');
40+
41+
expect(fs.existsSync(indexPath)).toBe(true);
42+
expect(fs.existsSync(denoJsonPath)).toBe(true);
43+
});
44+
45+
it('should create index.ts that imports GreetUser and starts EdgeWorker', async () => {
46+
await createExampleWorker({
47+
supabasePath,
48+
autoConfirm: true,
49+
});
50+
51+
const indexPath = path.join(workerDir, 'index.ts');
52+
const indexContent = fs.readFileSync(indexPath, 'utf8');
53+
54+
// Should import EdgeWorker
55+
expect(indexContent).toContain("import { EdgeWorker } from '@pgflow/edge-worker'");
56+
// Should import GreetUser from flows directory
57+
expect(indexContent).toContain("import { GreetUser } from '../../flows/greet-user.ts'");
58+
// Should start EdgeWorker with GreetUser
59+
expect(indexContent).toContain('EdgeWorker.start(GreetUser)');
60+
});
61+
62+
it('should create deno.json with correct import mappings', async () => {
63+
await createExampleWorker({
64+
supabasePath,
65+
autoConfirm: true,
66+
});
67+
68+
const denoJsonPath = path.join(workerDir, 'deno.json');
69+
const denoJsonContent = fs.readFileSync(denoJsonPath, 'utf8');
70+
const denoJson = JSON.parse(denoJsonContent);
71+
72+
// Verify imports exist
73+
expect(denoJson.imports).toBeDefined();
74+
expect(denoJson.imports['@pgflow/core']).toBeDefined();
75+
expect(denoJson.imports['@pgflow/dsl']).toBeDefined();
76+
expect(denoJson.imports['@pgflow/edge-worker']).toBeDefined();
77+
});
78+
79+
it('should inject package version instead of @latest in deno.json', async () => {
80+
await createExampleWorker({
81+
supabasePath,
82+
autoConfirm: true,
83+
});
84+
85+
const denoJsonPath = path.join(workerDir, 'deno.json');
86+
const denoJsonContent = fs.readFileSync(denoJsonPath, 'utf8');
87+
const denoJson = JSON.parse(denoJsonContent);
88+
89+
const version = getVersion();
90+
91+
// Verify version is not 'unknown'
92+
expect(version).not.toBe('unknown');
93+
94+
// Verify that @latest is NOT used
95+
expect(denoJsonContent).not.toContain('@latest');
96+
97+
// Verify that the actual version is used
98+
expect(denoJson.imports['@pgflow/core']).toBe(`npm:@pgflow/core@${version}`);
99+
expect(denoJson.imports['@pgflow/dsl']).toBe(`npm:@pgflow/dsl@${version}`);
100+
expect(denoJson.imports['@pgflow/edge-worker']).toBe(`jsr:@pgflow/edge-worker@${version}`);
101+
});
102+
103+
it('should not create files when they already exist', async () => {
104+
// Pre-create the directory and files
105+
fs.mkdirSync(workerDir, { recursive: true });
106+
107+
const indexPath = path.join(workerDir, 'index.ts');
108+
const denoJsonPath = path.join(workerDir, 'deno.json');
109+
110+
fs.writeFileSync(indexPath, '// existing content');
111+
fs.writeFileSync(denoJsonPath, '// existing content');
112+
113+
const result = await createExampleWorker({
114+
supabasePath,
115+
autoConfirm: true,
116+
});
117+
118+
// Should return false because no changes were needed
119+
expect(result).toBe(false);
120+
121+
// Verify files still exist with original content
122+
expect(fs.readFileSync(indexPath, 'utf8')).toBe('// existing content');
123+
expect(fs.readFileSync(denoJsonPath, 'utf8')).toBe('// existing content');
124+
});
125+
126+
it('should create only missing files when some already exist', async () => {
127+
// Pre-create the directory and one file
128+
fs.mkdirSync(workerDir, { recursive: true });
129+
130+
const indexPath = path.join(workerDir, 'index.ts');
131+
const denoJsonPath = path.join(workerDir, 'deno.json');
132+
133+
// Only create index.ts
134+
fs.writeFileSync(indexPath, '// existing content');
135+
136+
const result = await createExampleWorker({
137+
supabasePath,
138+
autoConfirm: true,
139+
});
140+
141+
// Should return true because deno.json was created
142+
expect(result).toBe(true);
143+
144+
// Verify index.ts was not modified
145+
expect(fs.readFileSync(indexPath, 'utf8')).toBe('// existing content');
146+
147+
// Verify deno.json was created
148+
expect(fs.existsSync(denoJsonPath)).toBe(true);
149+
150+
const denoJsonContent = fs.readFileSync(denoJsonPath, 'utf8');
151+
expect(denoJsonContent).toContain('"imports"');
152+
});
153+
154+
it('should create parent directories if they do not exist', async () => {
155+
// Don't create anything - let the function create it all
156+
expect(fs.existsSync(supabasePath)).toBe(false);
157+
158+
const result = await createExampleWorker({
159+
supabasePath,
160+
autoConfirm: true,
161+
});
162+
163+
expect(result).toBe(true);
164+
165+
// Verify all parent directories were created
166+
expect(fs.existsSync(supabasePath)).toBe(true);
167+
expect(fs.existsSync(path.join(supabasePath, 'functions'))).toBe(true);
168+
expect(fs.existsSync(workerDir)).toBe(true);
169+
170+
// Verify files exist
171+
expect(fs.existsSync(path.join(workerDir, 'index.ts'))).toBe(true);
172+
expect(fs.existsSync(path.join(workerDir, 'deno.json'))).toBe(true);
173+
});
174+
175+
it('should include subpath exports for Deno import mapping', async () => {
176+
await createExampleWorker({
177+
supabasePath,
178+
autoConfirm: true,
179+
});
180+
181+
const denoJsonPath = path.join(workerDir, 'deno.json');
182+
const denoJsonContent = fs.readFileSync(denoJsonPath, 'utf8');
183+
const denoJson = JSON.parse(denoJsonContent);
184+
185+
const version = getVersion();
186+
187+
// Verify subpath exports include versions (needed for proper Deno import mapping)
188+
expect(denoJson.imports['@pgflow/core/']).toBe(`npm:@pgflow/core@${version}/`);
189+
expect(denoJson.imports['@pgflow/dsl/']).toBe(`npm:@pgflow/dsl@${version}/`);
190+
expect(denoJson.imports['@pgflow/edge-worker/']).toBe(`jsr:@pgflow/edge-worker@${version}/`);
191+
});
192+
});

pkgs/cli/__tests__/commands/install/create-flows-directory.test.ts

Lines changed: 42 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ describe('createFlowsDirectory', () => {
3535

3636
// Verify all files exist
3737
const indexPath = path.join(flowsDir, 'index.ts');
38-
const exampleFlowPath = path.join(flowsDir, 'example-flow.ts');
38+
const greetUserPath = path.join(flowsDir, 'greet-user.ts');
3939

4040
expect(fs.existsSync(indexPath)).toBe(true);
41-
expect(fs.existsSync(exampleFlowPath)).toBe(true);
41+
expect(fs.existsSync(greetUserPath)).toBe(true);
4242
});
4343

4444
it('should create index.ts with barrel export pattern', async () => {
@@ -50,42 +50,60 @@ describe('createFlowsDirectory', () => {
5050
const indexPath = path.join(flowsDir, 'index.ts');
5151
const indexContent = fs.readFileSync(indexPath, 'utf8');
5252

53-
// Should have export for ExampleFlow
54-
expect(indexContent).toContain("export { ExampleFlow } from './example-flow.ts';");
53+
// Should have export for GreetUser
54+
expect(indexContent).toContain("export { GreetUser } from './greet-user.ts';");
5555
// Should have documenting comment
5656
expect(indexContent).toContain('Re-export all flows');
5757
});
5858

59-
it('should create example-flow.ts with named export', async () => {
59+
it('should create greet-user.ts with named export', async () => {
6060
await createFlowsDirectory({
6161
supabasePath,
6262
autoConfirm: true,
6363
});
6464

65-
const exampleFlowPath = path.join(flowsDir, 'example-flow.ts');
66-
const exampleFlowContent = fs.readFileSync(exampleFlowPath, 'utf8');
65+
const greetUserPath = path.join(flowsDir, 'greet-user.ts');
66+
const greetUserContent = fs.readFileSync(greetUserPath, 'utf8');
6767

6868
// Should use named export (not default)
69-
expect(exampleFlowContent).toContain('export const ExampleFlow');
69+
expect(greetUserContent).toContain('export const GreetUser');
7070
// Should import Flow from @pgflow/dsl
71-
expect(exampleFlowContent).toContain("import { Flow } from '@pgflow/dsl'");
71+
expect(greetUserContent).toContain("import { Flow } from '@pgflow/dsl'");
7272
// Should have correct slug
73-
expect(exampleFlowContent).toContain("slug: 'exampleFlow'");
74-
// Should have input type
75-
expect(exampleFlowContent).toContain('type Input');
76-
// Should have at least one step
77-
expect(exampleFlowContent).toContain('.step(');
73+
expect(greetUserContent).toContain("slug: 'greetUser'");
74+
// Should have input type with firstName and lastName
75+
expect(greetUserContent).toContain('type Input');
76+
expect(greetUserContent).toContain('firstName');
77+
expect(greetUserContent).toContain('lastName');
78+
});
79+
80+
it('should create greet-user.ts with two steps showing dependsOn', async () => {
81+
await createFlowsDirectory({
82+
supabasePath,
83+
autoConfirm: true,
84+
});
85+
86+
const greetUserPath = path.join(flowsDir, 'greet-user.ts');
87+
const greetUserContent = fs.readFileSync(greetUserPath, 'utf8');
88+
89+
// Should have two steps
90+
expect(greetUserContent).toContain("slug: 'fullName'");
91+
expect(greetUserContent).toContain("slug: 'greeting'");
92+
// Second step should depend on first
93+
expect(greetUserContent).toContain("dependsOn: ['fullName']");
94+
// Second step should access result from first step
95+
expect(greetUserContent).toContain('input.fullName');
7896
});
7997

8098
it('should not create files when they already exist', async () => {
8199
// Pre-create the directory and files
82100
fs.mkdirSync(flowsDir, { recursive: true });
83101

84102
const indexPath = path.join(flowsDir, 'index.ts');
85-
const exampleFlowPath = path.join(flowsDir, 'example-flow.ts');
103+
const greetUserPath = path.join(flowsDir, 'greet-user.ts');
86104

87105
fs.writeFileSync(indexPath, '// existing content');
88-
fs.writeFileSync(exampleFlowPath, '// existing content');
106+
fs.writeFileSync(greetUserPath, '// existing content');
89107

90108
const result = await createFlowsDirectory({
91109
supabasePath,
@@ -97,15 +115,15 @@ describe('createFlowsDirectory', () => {
97115

98116
// Verify files still exist with original content
99117
expect(fs.readFileSync(indexPath, 'utf8')).toBe('// existing content');
100-
expect(fs.readFileSync(exampleFlowPath, 'utf8')).toBe('// existing content');
118+
expect(fs.readFileSync(greetUserPath, 'utf8')).toBe('// existing content');
101119
});
102120

103121
it('should create only missing files when some already exist', async () => {
104122
// Pre-create the directory and one file
105123
fs.mkdirSync(flowsDir, { recursive: true });
106124

107125
const indexPath = path.join(flowsDir, 'index.ts');
108-
const exampleFlowPath = path.join(flowsDir, 'example-flow.ts');
126+
const greetUserPath = path.join(flowsDir, 'greet-user.ts');
109127

110128
// Only create index.ts
111129
fs.writeFileSync(indexPath, '// existing content');
@@ -115,17 +133,17 @@ describe('createFlowsDirectory', () => {
115133
autoConfirm: true,
116134
});
117135

118-
// Should return true because example-flow.ts was created
136+
// Should return true because greet-user.ts was created
119137
expect(result).toBe(true);
120138

121139
// Verify index.ts was not modified
122140
expect(fs.readFileSync(indexPath, 'utf8')).toBe('// existing content');
123141

124-
// Verify example-flow.ts was created
125-
expect(fs.existsSync(exampleFlowPath)).toBe(true);
142+
// Verify greet-user.ts was created
143+
expect(fs.existsSync(greetUserPath)).toBe(true);
126144

127-
const exampleContent = fs.readFileSync(exampleFlowPath, 'utf8');
128-
expect(exampleContent).toContain('export const ExampleFlow');
145+
const greetUserContent = fs.readFileSync(greetUserPath, 'utf8');
146+
expect(greetUserContent).toContain('export const GreetUser');
129147
});
130148

131149
it('should create parent directories if they do not exist', async () => {
@@ -145,6 +163,6 @@ describe('createFlowsDirectory', () => {
145163

146164
// Verify files exist
147165
expect(fs.existsSync(path.join(flowsDir, 'index.ts'))).toBe(true);
148-
expect(fs.existsSync(path.join(flowsDir, 'example-flow.ts'))).toBe(true);
166+
expect(fs.existsSync(path.join(flowsDir, 'greet-user.ts'))).toBe(true);
149167
});
150168
});

pkgs/cli/examples/async-function-hang.ts

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)