@effect-gql/cli 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin.js CHANGED
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { Effect, Console } from 'effect';
2
+ import { Effect, Console, Option, pipe } from 'effect';
3
3
  import { lexicographicSortSchema, printSchema } from '@effect-gql/core';
4
- import * as fs from 'fs';
5
- import * as path from 'path';
4
+ import * as fs3 from 'fs';
5
+ import * as path2 from 'path';
6
+ import * as readline from 'readline';
7
+ import { spawn } from 'child_process';
6
8
 
7
9
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
10
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -11,8 +13,8 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
11
13
  throw Error('Dynamic require of "' + x + '" is not supported');
12
14
  });
13
15
  var loadSchema = (modulePath) => Effect.gen(function* () {
14
- const absolutePath = path.resolve(process.cwd(), modulePath);
15
- if (!fs.existsSync(absolutePath)) {
16
+ const absolutePath = path2.resolve(process.cwd(), modulePath);
17
+ if (!fs3.existsSync(absolutePath)) {
16
18
  return yield* Effect.fail(new Error(`File not found: ${absolutePath}`));
17
19
  }
18
20
  const module = yield* Effect.tryPromise({
@@ -53,20 +55,20 @@ var run = (options) => Effect.gen(function* () {
53
55
  const schema = yield* loadSchema(options.modulePath);
54
56
  const sdl = generateSDL(schema, { sort: options.sort });
55
57
  if (options.output) {
56
- const outputPath = path.resolve(process.cwd(), options.output);
57
- fs.writeFileSync(outputPath, sdl);
58
+ const outputPath = path2.resolve(process.cwd(), options.output);
59
+ fs3.writeFileSync(outputPath, sdl);
58
60
  yield* Console.log(`Schema written to ${outputPath}`);
59
61
  } else {
60
62
  yield* Console.log(sdl);
61
63
  }
62
64
  });
63
65
  var watch2 = (options) => Effect.gen(function* () {
64
- const absolutePath = path.resolve(process.cwd(), options.modulePath);
65
- const dir = path.dirname(absolutePath);
66
+ const absolutePath = path2.resolve(process.cwd(), options.modulePath);
67
+ const dir = path2.dirname(absolutePath);
66
68
  yield* Console.log(`Watching for changes in ${dir}...`);
67
69
  yield* run(options).pipe(Effect.catchAll((error) => Console.error(`Error: ${error.message}`)));
68
70
  yield* Effect.async(() => {
69
- const watcher = fs.watch(dir, { recursive: true }, (_, filename) => {
71
+ const watcher = fs3.watch(dir, { recursive: true }, (_, filename) => {
70
72
  if (filename?.endsWith(".ts") || filename?.endsWith(".js")) {
71
73
  Effect.runPromise(
72
74
  run(options).pipe(
@@ -156,6 +158,1123 @@ var runGenerateSchema = (args) => Effect.gen(function* () {
156
158
  }
157
159
  });
158
160
 
161
+ // src/commands/create/types.ts
162
+ var SERVER_TYPES = ["node", "bun", "express", "web"];
163
+ var isValidServerType = (value) => SERVER_TYPES.includes(value);
164
+ var PACKAGE_MANAGERS = ["npm", "pnpm", "yarn", "bun"];
165
+ var isValidPackageManager = (value) => PACKAGE_MANAGERS.includes(value);
166
+
167
+ // src/commands/create/args.ts
168
+ var parseArgs2 = (args) => {
169
+ const positional = [];
170
+ let serverType;
171
+ let directory = Option.none();
172
+ let monorepo = Option.none();
173
+ let skipInstall = false;
174
+ let packageManager = Option.none();
175
+ let interactive = false;
176
+ for (let i = 0; i < args.length; i++) {
177
+ const arg = args[i];
178
+ if (arg === "-h" || arg === "--help") {
179
+ return { help: true };
180
+ } else if (arg === "-i" || arg === "--interactive") {
181
+ interactive = true;
182
+ } else if (arg === "-s" || arg === "--server-type") {
183
+ const value = args[++i];
184
+ if (!value) {
185
+ return { error: "Missing value for --server-type" };
186
+ }
187
+ if (!isValidServerType(value)) {
188
+ return {
189
+ error: `Invalid server type: ${value}. Must be one of: ${SERVER_TYPES.join(", ")}`
190
+ };
191
+ }
192
+ serverType = value;
193
+ } else if (arg === "-d" || arg === "--directory") {
194
+ const value = args[++i];
195
+ if (!value) {
196
+ return { error: "Missing value for --directory" };
197
+ }
198
+ directory = Option.some(value);
199
+ } else if (arg === "--monorepo") {
200
+ monorepo = Option.some(true);
201
+ } else if (arg === "--skip-install") {
202
+ skipInstall = true;
203
+ } else if (arg === "--package-manager") {
204
+ const value = args[++i];
205
+ if (!value) {
206
+ return { error: "Missing value for --package-manager" };
207
+ }
208
+ if (!isValidPackageManager(value)) {
209
+ return { error: `Invalid package manager: ${value}. Must be one of: npm, pnpm, yarn, bun` };
210
+ }
211
+ packageManager = Option.some(value);
212
+ } else if (!arg.startsWith("-")) {
213
+ positional.push(arg);
214
+ } else {
215
+ return { error: `Unknown option: ${arg}` };
216
+ }
217
+ }
218
+ if (interactive) {
219
+ return { interactive: true };
220
+ }
221
+ if (args.length === 0) {
222
+ return { interactive: true };
223
+ }
224
+ if (positional.length === 0) {
225
+ return {
226
+ error: "Missing project name. Use --interactive or provide: effect-gql create <name> --server-type <type>"
227
+ };
228
+ }
229
+ if (!serverType) {
230
+ return { error: "Missing --server-type. Must be one of: " + SERVER_TYPES.join(", ") };
231
+ }
232
+ const options = {
233
+ name: positional[0],
234
+ serverType,
235
+ directory,
236
+ monorepo,
237
+ skipInstall,
238
+ packageManager
239
+ };
240
+ return { options };
241
+ };
242
+
243
+ // src/commands/create/templates/package-json.ts
244
+ var VERSIONS = {
245
+ core: "^1.1.0",
246
+ node: "^1.1.0",
247
+ bun: "^1.1.0",
248
+ express: "^1.1.0",
249
+ web: "^1.1.0",
250
+ effect: "^3.19.0",
251
+ platform: "^0.94.0",
252
+ platformNode: "^0.104.0",
253
+ platformBun: "^0.87.0",
254
+ graphql: "^16.0.0",
255
+ expressLib: "^5.0.0",
256
+ tsx: "^4.19.0",
257
+ typescript: "^5.0.0"
258
+ };
259
+ var getDependencies = (serverType, isMonorepo) => {
260
+ const toVersion = (pkg, version) => isMonorepo && pkg.startsWith("@effect-gql/") ? "workspace:*" : version;
261
+ const baseDeps = {
262
+ "@effect-gql/core": toVersion("@effect-gql/core", VERSIONS.core),
263
+ "@effect/platform": VERSIONS.platform,
264
+ effect: VERSIONS.effect,
265
+ graphql: VERSIONS.graphql
266
+ };
267
+ const baseDevDeps = {
268
+ tsx: VERSIONS.tsx,
269
+ typescript: VERSIONS.typescript
270
+ };
271
+ switch (serverType) {
272
+ case "node":
273
+ return {
274
+ dependencies: {
275
+ ...baseDeps,
276
+ "@effect-gql/node": toVersion("@effect-gql/node", VERSIONS.node),
277
+ "@effect/platform-node": VERSIONS.platformNode
278
+ },
279
+ devDependencies: baseDevDeps
280
+ };
281
+ case "bun":
282
+ return {
283
+ dependencies: {
284
+ ...baseDeps,
285
+ "@effect-gql/bun": toVersion("@effect-gql/bun", VERSIONS.bun),
286
+ "@effect/platform-bun": VERSIONS.platformBun
287
+ },
288
+ devDependencies: {
289
+ typescript: VERSIONS.typescript
290
+ // Bun has built-in TypeScript support, no tsx needed
291
+ }
292
+ };
293
+ case "express":
294
+ return {
295
+ dependencies: {
296
+ ...baseDeps,
297
+ "@effect-gql/express": toVersion("@effect-gql/express", VERSIONS.express),
298
+ express: VERSIONS.expressLib
299
+ },
300
+ devDependencies: {
301
+ ...baseDevDeps,
302
+ "@types/express": "^5.0.0"
303
+ }
304
+ };
305
+ case "web":
306
+ return {
307
+ dependencies: {
308
+ ...baseDeps,
309
+ "@effect-gql/web": toVersion("@effect-gql/web", VERSIONS.web)
310
+ },
311
+ devDependencies: baseDevDeps
312
+ };
313
+ }
314
+ };
315
+ var getScripts = (serverType) => {
316
+ switch (serverType) {
317
+ case "node":
318
+ return {
319
+ start: "tsx src/index.ts",
320
+ dev: "tsx watch src/index.ts",
321
+ build: "tsc",
322
+ typecheck: "tsc --noEmit"
323
+ };
324
+ case "bun":
325
+ return {
326
+ start: "bun run src/index.ts",
327
+ dev: "bun --watch src/index.ts",
328
+ build: "bun build src/index.ts --outdir dist --target bun",
329
+ typecheck: "tsc --noEmit"
330
+ };
331
+ case "express":
332
+ return {
333
+ start: "tsx src/index.ts",
334
+ dev: "tsx watch src/index.ts",
335
+ build: "tsc",
336
+ typecheck: "tsc --noEmit"
337
+ };
338
+ case "web":
339
+ return {
340
+ start: "tsx src/index.ts",
341
+ dev: "tsx watch src/index.ts",
342
+ build: "tsc",
343
+ typecheck: "tsc --noEmit"
344
+ // Users will typically add wrangler/deno commands as needed
345
+ };
346
+ }
347
+ };
348
+ var generatePackageJson = (ctx) => {
349
+ const deps = getDependencies(ctx.serverType, ctx.isMonorepo);
350
+ const pkg = {
351
+ name: ctx.name,
352
+ version: "0.0.1",
353
+ private: true,
354
+ type: "module",
355
+ scripts: getScripts(ctx.serverType),
356
+ dependencies: deps.dependencies,
357
+ devDependencies: deps.devDependencies
358
+ };
359
+ return JSON.stringify(pkg, null, 2) + "\n";
360
+ };
361
+
362
+ // src/commands/create/templates/tsconfig.ts
363
+ var generateTsConfig = (ctx) => {
364
+ const config = ctx.isMonorepo ? {
365
+ // In a monorepo, extend from root tsconfig
366
+ extends: "../../tsconfig.json",
367
+ compilerOptions: {
368
+ outDir: "./dist",
369
+ rootDir: "./src",
370
+ noEmit: true
371
+ },
372
+ include: ["src/**/*"],
373
+ exclude: ["node_modules", "dist"]
374
+ } : {
375
+ // Standalone project config
376
+ compilerOptions: {
377
+ target: "ES2022",
378
+ module: "NodeNext",
379
+ moduleResolution: "NodeNext",
380
+ lib: ["ES2022"],
381
+ outDir: "./dist",
382
+ rootDir: "./src",
383
+ strict: true,
384
+ esModuleInterop: true,
385
+ skipLibCheck: true,
386
+ forceConsistentCasingInFileNames: true,
387
+ declaration: true,
388
+ declarationMap: true,
389
+ sourceMap: true,
390
+ noEmit: true,
391
+ // Effect requires these for decorators
392
+ experimentalDecorators: true,
393
+ emitDecoratorMetadata: true
394
+ },
395
+ include: ["src/**/*"],
396
+ exclude: ["node_modules", "dist"]
397
+ };
398
+ return JSON.stringify(config, null, 2) + "\n";
399
+ };
400
+
401
+ // src/commands/create/templates/server.ts
402
+ var generateNodeServer = (ctx) => `/**
403
+ * ${ctx.name} - GraphQL Server
404
+ *
405
+ * A GraphQL server built with Effect GQL and Node.js.
406
+ */
407
+
408
+ import { Effect, Layer } from "effect"
409
+ import * as S from "effect/Schema"
410
+ import { HttpRouter, HttpServerResponse } from "@effect/platform"
411
+ import { GraphQLSchemaBuilder, query, mutation, makeGraphQLRouter } from "@effect-gql/core"
412
+ import { serve } from "@effect-gql/node"
413
+
414
+ // =============================================================================
415
+ // Domain Models
416
+ // =============================================================================
417
+
418
+ /**
419
+ * Define your types with Effect Schema.
420
+ * This single definition is used for both TypeScript types AND GraphQL types.
421
+ */
422
+ const User = S.Struct({
423
+ id: S.String,
424
+ name: S.String,
425
+ email: S.String,
426
+ })
427
+
428
+ type User = S.Schema.Type<typeof User>
429
+
430
+ // =============================================================================
431
+ // In-Memory Data Store (replace with your database)
432
+ // =============================================================================
433
+
434
+ const users: User[] = [
435
+ { id: "1", name: "Alice", email: "alice@example.com" },
436
+ { id: "2", name: "Bob", email: "bob@example.com" },
437
+ ]
438
+
439
+ // =============================================================================
440
+ // GraphQL Schema
441
+ // =============================================================================
442
+
443
+ const schema = GraphQLSchemaBuilder.empty
444
+ .pipe(
445
+ // Simple query
446
+ query("hello", {
447
+ type: S.String,
448
+ description: "Returns a friendly greeting",
449
+ resolve: () => Effect.succeed("Hello from ${ctx.name}!"),
450
+ }),
451
+
452
+ // Query with arguments
453
+ query("user", {
454
+ args: S.Struct({ id: S.String }),
455
+ type: S.NullOr(User),
456
+ description: "Get a user by ID",
457
+ resolve: (args) => Effect.succeed(users.find((u) => u.id === args.id) ?? null),
458
+ }),
459
+
460
+ // Query that returns a list
461
+ query("users", {
462
+ type: S.Array(User),
463
+ description: "Get all users",
464
+ resolve: () => Effect.succeed(users),
465
+ }),
466
+
467
+ // Mutation
468
+ mutation("createUser", {
469
+ args: S.Struct({
470
+ name: S.String,
471
+ email: S.String,
472
+ }),
473
+ type: User,
474
+ description: "Create a new user",
475
+ resolve: (args) =>
476
+ Effect.sync(() => {
477
+ const newUser: User = {
478
+ id: String(users.length + 1),
479
+ name: args.name,
480
+ email: args.email,
481
+ }
482
+ users.push(newUser)
483
+ return newUser
484
+ }),
485
+ })
486
+ )
487
+ .buildSchema()
488
+
489
+ // =============================================================================
490
+ // HTTP Router
491
+ // =============================================================================
492
+
493
+ const graphqlRouter = makeGraphQLRouter(schema, Layer.empty, {
494
+ path: "/graphql",
495
+ graphiql: {
496
+ path: "/graphiql",
497
+ endpoint: "/graphql",
498
+ },
499
+ })
500
+
501
+ const router = HttpRouter.empty.pipe(
502
+ HttpRouter.get("/health", HttpServerResponse.json({ status: "ok" })),
503
+ HttpRouter.concat(graphqlRouter)
504
+ )
505
+
506
+ // =============================================================================
507
+ // Server Startup
508
+ // =============================================================================
509
+
510
+ serve(router, Layer.empty, {
511
+ port: 4000,
512
+ onStart: (url: string) => {
513
+ console.log(\`Server ready at \${url}\`)
514
+ console.log(\`GraphQL endpoint: \${url}/graphql\`)
515
+ console.log(\`GraphiQL playground: \${url}/graphiql\`)
516
+ console.log(\`Health check: \${url}/health\`)
517
+ },
518
+ })
519
+ `;
520
+ var generateBunServer = (ctx) => `/**
521
+ * ${ctx.name} - GraphQL Server
522
+ *
523
+ * A GraphQL server built with Effect GQL and Bun.
524
+ */
525
+
526
+ import { Effect, Layer } from "effect"
527
+ import * as S from "effect/Schema"
528
+ import { HttpRouter, HttpServerResponse } from "@effect/platform"
529
+ import { GraphQLSchemaBuilder, query, mutation, makeGraphQLRouter } from "@effect-gql/core"
530
+ import { serve } from "@effect-gql/bun"
531
+
532
+ // =============================================================================
533
+ // Domain Models
534
+ // =============================================================================
535
+
536
+ const User = S.Struct({
537
+ id: S.String,
538
+ name: S.String,
539
+ email: S.String,
540
+ })
541
+
542
+ type User = S.Schema.Type<typeof User>
543
+
544
+ // =============================================================================
545
+ // In-Memory Data Store (replace with your database)
546
+ // =============================================================================
547
+
548
+ const users: User[] = [
549
+ { id: "1", name: "Alice", email: "alice@example.com" },
550
+ { id: "2", name: "Bob", email: "bob@example.com" },
551
+ ]
552
+
553
+ // =============================================================================
554
+ // GraphQL Schema
555
+ // =============================================================================
556
+
557
+ const schema = GraphQLSchemaBuilder.empty
558
+ .pipe(
559
+ query("hello", {
560
+ type: S.String,
561
+ description: "Returns a friendly greeting",
562
+ resolve: () => Effect.succeed("Hello from ${ctx.name}!"),
563
+ }),
564
+
565
+ query("user", {
566
+ args: S.Struct({ id: S.String }),
567
+ type: S.NullOr(User),
568
+ description: "Get a user by ID",
569
+ resolve: (args) => Effect.succeed(users.find((u) => u.id === args.id) ?? null),
570
+ }),
571
+
572
+ query("users", {
573
+ type: S.Array(User),
574
+ description: "Get all users",
575
+ resolve: () => Effect.succeed(users),
576
+ }),
577
+
578
+ mutation("createUser", {
579
+ args: S.Struct({
580
+ name: S.String,
581
+ email: S.String,
582
+ }),
583
+ type: User,
584
+ description: "Create a new user",
585
+ resolve: (args) =>
586
+ Effect.sync(() => {
587
+ const newUser: User = {
588
+ id: String(users.length + 1),
589
+ name: args.name,
590
+ email: args.email,
591
+ }
592
+ users.push(newUser)
593
+ return newUser
594
+ }),
595
+ })
596
+ )
597
+ .buildSchema()
598
+
599
+ // =============================================================================
600
+ // HTTP Router
601
+ // =============================================================================
602
+
603
+ const graphqlRouter = makeGraphQLRouter(schema, Layer.empty, {
604
+ path: "/graphql",
605
+ graphiql: {
606
+ path: "/graphiql",
607
+ endpoint: "/graphql",
608
+ },
609
+ })
610
+
611
+ const router = HttpRouter.empty.pipe(
612
+ HttpRouter.get("/health", HttpServerResponse.json({ status: "ok" })),
613
+ HttpRouter.concat(graphqlRouter)
614
+ )
615
+
616
+ // =============================================================================
617
+ // Server Startup
618
+ // =============================================================================
619
+
620
+ serve(router, Layer.empty, {
621
+ port: 4000,
622
+ onStart: (url: string) => {
623
+ console.log(\`Server ready at \${url}\`)
624
+ console.log(\`GraphQL endpoint: \${url}/graphql\`)
625
+ console.log(\`GraphiQL playground: \${url}/graphiql\`)
626
+ console.log(\`Health check: \${url}/health\`)
627
+ },
628
+ })
629
+ `;
630
+ var generateExpressServer = (ctx) => `/**
631
+ * ${ctx.name} - GraphQL Server
632
+ *
633
+ * A GraphQL server built with Effect GQL and Express.
634
+ */
635
+
636
+ import express from "express"
637
+ import { Effect, Layer } from "effect"
638
+ import * as S from "effect/Schema"
639
+ import { GraphQLSchemaBuilder, query, mutation, makeGraphQLRouter } from "@effect-gql/core"
640
+ import { toMiddleware } from "@effect-gql/express"
641
+
642
+ // =============================================================================
643
+ // Domain Models
644
+ // =============================================================================
645
+
646
+ const User = S.Struct({
647
+ id: S.String,
648
+ name: S.String,
649
+ email: S.String,
650
+ })
651
+
652
+ type User = S.Schema.Type<typeof User>
653
+
654
+ // =============================================================================
655
+ // In-Memory Data Store (replace with your database)
656
+ // =============================================================================
657
+
658
+ const users: User[] = [
659
+ { id: "1", name: "Alice", email: "alice@example.com" },
660
+ { id: "2", name: "Bob", email: "bob@example.com" },
661
+ ]
662
+
663
+ // =============================================================================
664
+ // GraphQL Schema
665
+ // =============================================================================
666
+
667
+ const schema = GraphQLSchemaBuilder.empty
668
+ .pipe(
669
+ query("hello", {
670
+ type: S.String,
671
+ description: "Returns a friendly greeting",
672
+ resolve: () => Effect.succeed("Hello from ${ctx.name}!"),
673
+ }),
674
+
675
+ query("user", {
676
+ args: S.Struct({ id: S.String }),
677
+ type: S.NullOr(User),
678
+ description: "Get a user by ID",
679
+ resolve: (args) => Effect.succeed(users.find((u) => u.id === args.id) ?? null),
680
+ }),
681
+
682
+ query("users", {
683
+ type: S.Array(User),
684
+ description: "Get all users",
685
+ resolve: () => Effect.succeed(users),
686
+ }),
687
+
688
+ mutation("createUser", {
689
+ args: S.Struct({
690
+ name: S.String,
691
+ email: S.String,
692
+ }),
693
+ type: User,
694
+ description: "Create a new user",
695
+ resolve: (args) =>
696
+ Effect.sync(() => {
697
+ const newUser: User = {
698
+ id: String(users.length + 1),
699
+ name: args.name,
700
+ email: args.email,
701
+ }
702
+ users.push(newUser)
703
+ return newUser
704
+ }),
705
+ })
706
+ )
707
+ .buildSchema()
708
+
709
+ // =============================================================================
710
+ // GraphQL Router
711
+ // =============================================================================
712
+
713
+ const graphqlRouter = makeGraphQLRouter(schema, Layer.empty, {
714
+ path: "/graphql",
715
+ graphiql: {
716
+ path: "/graphiql",
717
+ endpoint: "/graphql",
718
+ },
719
+ })
720
+
721
+ // =============================================================================
722
+ // Express App
723
+ // =============================================================================
724
+
725
+ const app = express()
726
+ app.use(express.json())
727
+
728
+ // Health check
729
+ app.get("/health", (_, res) => {
730
+ res.json({ status: "ok" })
731
+ })
732
+
733
+ // Mount GraphQL middleware
734
+ app.use(toMiddleware(graphqlRouter, Layer.empty))
735
+
736
+ // =============================================================================
737
+ // Server Startup
738
+ // =============================================================================
739
+
740
+ const port = process.env.PORT || 4000
741
+
742
+ app.listen(port, () => {
743
+ console.log(\`Server ready at http://localhost:\${port}\`)
744
+ console.log(\`GraphQL endpoint: http://localhost:\${port}/graphql\`)
745
+ console.log(\`GraphiQL playground: http://localhost:\${port}/graphiql\`)
746
+ console.log(\`Health check: http://localhost:\${port}/health\`)
747
+ })
748
+ `;
749
+ var generateWebHandler = (ctx) => `/**
750
+ * ${ctx.name} - GraphQL Server
751
+ *
752
+ * A GraphQL server built with Effect GQL using Web standard APIs.
753
+ * Compatible with Cloudflare Workers, Deno, and other WASM runtimes.
754
+ */
755
+
756
+ import { Effect, Layer } from "effect"
757
+ import * as S from "effect/Schema"
758
+ import { GraphQLSchemaBuilder, query, mutation, makeGraphQLRouter } from "@effect-gql/core"
759
+ import { toHandler } from "@effect-gql/web"
760
+
761
+ // =============================================================================
762
+ // Domain Models
763
+ // =============================================================================
764
+
765
+ const User = S.Struct({
766
+ id: S.String,
767
+ name: S.String,
768
+ email: S.String,
769
+ })
770
+
771
+ type User = S.Schema.Type<typeof User>
772
+
773
+ // =============================================================================
774
+ // In-Memory Data Store (replace with your database)
775
+ // =============================================================================
776
+
777
+ const users: User[] = [
778
+ { id: "1", name: "Alice", email: "alice@example.com" },
779
+ { id: "2", name: "Bob", email: "bob@example.com" },
780
+ ]
781
+
782
+ // =============================================================================
783
+ // GraphQL Schema
784
+ // =============================================================================
785
+
786
+ const schema = GraphQLSchemaBuilder.empty
787
+ .pipe(
788
+ query("hello", {
789
+ type: S.String,
790
+ description: "Returns a friendly greeting",
791
+ resolve: () => Effect.succeed("Hello from ${ctx.name}!"),
792
+ }),
793
+
794
+ query("user", {
795
+ args: S.Struct({ id: S.String }),
796
+ type: S.NullOr(User),
797
+ description: "Get a user by ID",
798
+ resolve: (args) => Effect.succeed(users.find((u) => u.id === args.id) ?? null),
799
+ }),
800
+
801
+ query("users", {
802
+ type: S.Array(User),
803
+ description: "Get all users",
804
+ resolve: () => Effect.succeed(users),
805
+ }),
806
+
807
+ mutation("createUser", {
808
+ args: S.Struct({
809
+ name: S.String,
810
+ email: S.String,
811
+ }),
812
+ type: User,
813
+ description: "Create a new user",
814
+ resolve: (args) =>
815
+ Effect.sync(() => {
816
+ const newUser: User = {
817
+ id: String(users.length + 1),
818
+ name: args.name,
819
+ email: args.email,
820
+ }
821
+ users.push(newUser)
822
+ return newUser
823
+ }),
824
+ })
825
+ )
826
+ .buildSchema()
827
+
828
+ // =============================================================================
829
+ // GraphQL Router
830
+ // =============================================================================
831
+
832
+ const graphqlRouter = makeGraphQLRouter(schema, Layer.empty, {
833
+ path: "/graphql",
834
+ graphiql: {
835
+ path: "/graphiql",
836
+ endpoint: "/graphql",
837
+ },
838
+ })
839
+
840
+ // =============================================================================
841
+ // Web Handler
842
+ // =============================================================================
843
+
844
+ const { handler } = toHandler(graphqlRouter, Layer.empty)
845
+
846
+ /**
847
+ * Export for Cloudflare Workers
848
+ */
849
+ export default {
850
+ async fetch(request: Request): Promise<Response> {
851
+ const url = new URL(request.url)
852
+
853
+ // Health check
854
+ if (url.pathname === "/health") {
855
+ return new Response(JSON.stringify({ status: "ok" }), {
856
+ headers: { "Content-Type": "application/json" },
857
+ })
858
+ }
859
+
860
+ return handler(request)
861
+ },
862
+ }
863
+
864
+ /**
865
+ * For local development with Deno or other runtimes, you can use:
866
+ *
867
+ * Deno.serve((request) => {
868
+ * const url = new URL(request.url)
869
+ * if (url.pathname === "/health") {
870
+ * return new Response(JSON.stringify({ status: "ok" }), {
871
+ * headers: { "Content-Type": "application/json" },
872
+ * })
873
+ * }
874
+ * return handler(request)
875
+ * })
876
+ */
877
+ `;
878
+ var generateServerTemplate = (ctx) => {
879
+ switch (ctx.serverType) {
880
+ case "node":
881
+ return generateNodeServer(ctx);
882
+ case "bun":
883
+ return generateBunServer(ctx);
884
+ case "express":
885
+ return generateExpressServer(ctx);
886
+ case "web":
887
+ return generateWebHandler(ctx);
888
+ }
889
+ };
890
+
891
+ // src/commands/create/templates/index.ts
892
+ var generateGitignore = () => `# Dependencies
893
+ node_modules/
894
+
895
+ # Build output
896
+ dist/
897
+ *.tsbuildinfo
898
+
899
+ # IDE
900
+ .idea/
901
+ .vscode/
902
+ *.swp
903
+ *.swo
904
+
905
+ # OS
906
+ .DS_Store
907
+ Thumbs.db
908
+
909
+ # Environment
910
+ .env
911
+ .env.local
912
+ .env.*.local
913
+
914
+ # Logs
915
+ *.log
916
+ npm-debug.log*
917
+ yarn-debug.log*
918
+ yarn-error.log*
919
+
920
+ # Testing
921
+ coverage/
922
+ `;
923
+ var generateProject = (ctx) => [
924
+ {
925
+ path: "package.json",
926
+ content: generatePackageJson(ctx)
927
+ },
928
+ {
929
+ path: "tsconfig.json",
930
+ content: generateTsConfig(ctx)
931
+ },
932
+ {
933
+ path: "src/index.ts",
934
+ content: generateServerTemplate(ctx)
935
+ },
936
+ {
937
+ path: ".gitignore",
938
+ content: generateGitignore()
939
+ }
940
+ ];
941
+ var fileExists = (filePath) => Effect.sync(() => fs3.existsSync(filePath));
942
+ var readFile = (filePath) => Effect.try({
943
+ try: () => fs3.readFileSync(filePath, "utf-8"),
944
+ catch: (error) => new Error(`Failed to read ${filePath}: ${error}`)
945
+ });
946
+ var detectPackageManager = (dir) => Effect.gen(function* () {
947
+ if (yield* fileExists(path2.join(dir, "pnpm-lock.yaml"))) return "pnpm";
948
+ if (yield* fileExists(path2.join(dir, "bun.lockb"))) return "bun";
949
+ if (yield* fileExists(path2.join(dir, "yarn.lock"))) return "yarn";
950
+ return "npm";
951
+ });
952
+ var detectMonorepo = (targetDir) => Effect.gen(function* () {
953
+ let currentDir = path2.dirname(path2.resolve(targetDir));
954
+ const root = path2.parse(currentDir).root;
955
+ while (currentDir !== root) {
956
+ const pnpmWorkspacePath = path2.join(currentDir, "pnpm-workspace.yaml");
957
+ if (yield* fileExists(pnpmWorkspacePath)) {
958
+ return {
959
+ isMonorepo: true,
960
+ packageManager: Option.some("pnpm"),
961
+ workspacesRoot: Option.some(currentDir)
962
+ };
963
+ }
964
+ const pkgPath = path2.join(currentDir, "package.json");
965
+ if (yield* fileExists(pkgPath)) {
966
+ const content = yield* readFile(pkgPath).pipe(Effect.catchAll(() => Effect.succeed("")));
967
+ if (content) {
968
+ try {
969
+ const pkg = JSON.parse(content);
970
+ if (pkg.workspaces) {
971
+ const pm = yield* detectPackageManager(currentDir);
972
+ return {
973
+ isMonorepo: true,
974
+ packageManager: Option.some(pm),
975
+ workspacesRoot: Option.some(currentDir)
976
+ };
977
+ }
978
+ } catch {
979
+ }
980
+ }
981
+ }
982
+ const turboPath = path2.join(currentDir, "turbo.json");
983
+ if (yield* fileExists(turboPath)) {
984
+ const pm = yield* detectPackageManager(currentDir);
985
+ return {
986
+ isMonorepo: true,
987
+ packageManager: Option.some(pm),
988
+ workspacesRoot: Option.some(currentDir)
989
+ };
990
+ }
991
+ currentDir = path2.dirname(currentDir);
992
+ }
993
+ return {
994
+ isMonorepo: false,
995
+ packageManager: Option.none(),
996
+ workspacesRoot: Option.none()
997
+ };
998
+ });
999
+ var getInstallCommand = (pm) => {
1000
+ switch (pm) {
1001
+ case "npm":
1002
+ return "npm install";
1003
+ case "pnpm":
1004
+ return "pnpm install";
1005
+ case "yarn":
1006
+ return "yarn";
1007
+ case "bun":
1008
+ return "bun install";
1009
+ }
1010
+ };
1011
+ var getRunPrefix = (pm) => {
1012
+ switch (pm) {
1013
+ case "npm":
1014
+ return "npm run";
1015
+ case "pnpm":
1016
+ return "pnpm";
1017
+ case "yarn":
1018
+ return "yarn";
1019
+ case "bun":
1020
+ return "bun run";
1021
+ }
1022
+ };
1023
+
1024
+ // src/commands/create/scaffolder.ts
1025
+ var validateDirectory = (targetDir) => Effect.try({
1026
+ try: () => {
1027
+ if (fs3.existsSync(targetDir)) {
1028
+ const files = fs3.readdirSync(targetDir);
1029
+ if (files.length > 0) {
1030
+ throw new Error(`Directory ${targetDir} is not empty`);
1031
+ }
1032
+ }
1033
+ },
1034
+ catch: (error) => error instanceof Error ? error : new Error(`Failed to validate directory: ${error}`)
1035
+ });
1036
+ var mkdirp = (dir) => Effect.try({
1037
+ try: () => fs3.mkdirSync(dir, { recursive: true }),
1038
+ catch: (error) => new Error(`Failed to create directory ${dir}: ${error}`)
1039
+ });
1040
+ var writeFile = (filePath, content) => Effect.gen(function* () {
1041
+ yield* mkdirp(path2.dirname(filePath));
1042
+ yield* Effect.try({
1043
+ try: () => fs3.writeFileSync(filePath, content, "utf-8"),
1044
+ catch: (error) => new Error(`Failed to write ${filePath}: ${error}`)
1045
+ });
1046
+ });
1047
+ var runCommand = (command, args, cwd) => Effect.async((resume) => {
1048
+ const child = spawn(command, args, {
1049
+ cwd,
1050
+ stdio: "inherit",
1051
+ shell: true
1052
+ });
1053
+ child.on("close", (code) => {
1054
+ if (code === 0) {
1055
+ resume(Effect.succeed(void 0));
1056
+ } else {
1057
+ resume(Effect.fail(new Error(`Command failed with exit code ${code}`)));
1058
+ }
1059
+ });
1060
+ child.on("error", (error) => {
1061
+ resume(Effect.fail(new Error(`Failed to run command: ${error.message}`)));
1062
+ });
1063
+ return Effect.sync(() => {
1064
+ child.kill();
1065
+ });
1066
+ });
1067
+ var installDependencies = (targetDir, packageManager) => Effect.gen(function* () {
1068
+ yield* Console.log("");
1069
+ yield* Console.log("Installing dependencies...");
1070
+ const installCmd = getInstallCommand(packageManager);
1071
+ const [cmd, ...args] = installCmd.split(" ");
1072
+ yield* runCommand(cmd, args, targetDir);
1073
+ yield* Console.log("Dependencies installed successfully!");
1074
+ });
1075
+ var printSuccessMessage = (ctx, targetDir) => Effect.gen(function* () {
1076
+ const relativePath = path2.relative(process.cwd(), targetDir) || ".";
1077
+ const runPrefix = getRunPrefix(ctx.packageManager);
1078
+ yield* Console.log("");
1079
+ yield* Console.log(`Successfully created ${ctx.name}!`);
1080
+ yield* Console.log("");
1081
+ yield* Console.log("Next steps:");
1082
+ yield* Console.log(` cd ${relativePath}`);
1083
+ yield* Console.log(` ${runPrefix} dev`);
1084
+ yield* Console.log("");
1085
+ yield* Console.log("Your GraphQL server will be available at:");
1086
+ yield* Console.log(" http://localhost:4000/graphql");
1087
+ yield* Console.log(" http://localhost:4000/graphiql (playground)");
1088
+ yield* Console.log("");
1089
+ });
1090
+ var scaffold = (options) => Effect.gen(function* () {
1091
+ const targetDir = pipe(
1092
+ options.directory,
1093
+ Option.map((dir) => path2.resolve(process.cwd(), dir)),
1094
+ Option.getOrElse(() => path2.resolve(process.cwd(), options.name))
1095
+ );
1096
+ const monorepoInfo = yield* detectMonorepo(targetDir);
1097
+ const isMonorepo = pipe(
1098
+ options.monorepo,
1099
+ Option.getOrElse(() => monorepoInfo.isMonorepo)
1100
+ );
1101
+ const packageManager = yield* pipe(
1102
+ options.packageManager,
1103
+ Option.orElse(() => monorepoInfo.packageManager),
1104
+ Option.match({
1105
+ onNone: () => detectPackageManager(process.cwd()),
1106
+ onSome: (pm) => Effect.succeed(pm)
1107
+ })
1108
+ );
1109
+ const ctx = {
1110
+ name: options.name,
1111
+ serverType: options.serverType,
1112
+ isMonorepo,
1113
+ packageManager
1114
+ };
1115
+ yield* Console.log("");
1116
+ yield* Console.log(`Creating ${ctx.name} with ${ctx.serverType} server...`);
1117
+ yield* validateDirectory(targetDir);
1118
+ const files = generateProject(ctx);
1119
+ for (const file of files) {
1120
+ const filePath = path2.join(targetDir, file.path);
1121
+ yield* writeFile(filePath, file.content);
1122
+ yield* Console.log(` Created ${file.path}`);
1123
+ }
1124
+ if (!options.skipInstall) {
1125
+ yield* installDependencies(targetDir, packageManager).pipe(
1126
+ Effect.catchAll(
1127
+ (error) => Console.log(`Warning: Failed to install dependencies: ${error.message}`).pipe(
1128
+ Effect.andThen(
1129
+ Console.log(
1130
+ "You can install them manually by running: " + getInstallCommand(packageManager)
1131
+ )
1132
+ )
1133
+ )
1134
+ )
1135
+ );
1136
+ } else {
1137
+ yield* Console.log("");
1138
+ yield* Console.log(
1139
+ `Skipping dependency installation. Run '${getInstallCommand(packageManager)}' to install.`
1140
+ );
1141
+ }
1142
+ yield* printSuccessMessage(ctx, targetDir);
1143
+ });
1144
+
1145
+ // src/commands/create/index.ts
1146
+ var printCreateHelp = () => {
1147
+ console.log(`
1148
+ Usage: effect-gql create <name> --server-type <type> [options]
1149
+
1150
+ Create a new Effect GraphQL project.
1151
+
1152
+ Arguments:
1153
+ name Package/project name
1154
+
1155
+ Required Options:
1156
+ -s, --server-type Server type: node, bun, express, web
1157
+
1158
+ Optional Options:
1159
+ -d, --directory Target directory (default: ./<name>)
1160
+ --monorepo Create as workspace package (auto-detected)
1161
+ --skip-install Skip dependency installation
1162
+ --package-manager Package manager: npm, pnpm, yarn, bun
1163
+ -i, --interactive Interactive mode (prompts for options)
1164
+ -h, --help Show this help message
1165
+
1166
+ Server Types:
1167
+ node Node.js server using @effect-gql/node
1168
+ Best for: General Node.js deployments
1169
+
1170
+ bun Bun server using @effect-gql/bun
1171
+ Best for: Bun runtime with native WebSocket support
1172
+
1173
+ express Express middleware using @effect-gql/express
1174
+ Best for: Integrating into existing Express apps
1175
+
1176
+ web Web standard handler using @effect-gql/web
1177
+ Best for: Cloudflare Workers, Deno, edge runtimes
1178
+
1179
+ Examples:
1180
+ effect-gql create my-api --server-type node
1181
+ effect-gql create my-api -s bun
1182
+ effect-gql create my-api -s express -d ./packages/api --monorepo
1183
+ effect-gql create my-api -s web --skip-install
1184
+ effect-gql create --interactive
1185
+ `);
1186
+ };
1187
+ var prompt = (question) => Effect.async((resume) => {
1188
+ const rl = readline.createInterface({
1189
+ input: process.stdin,
1190
+ output: process.stdout
1191
+ });
1192
+ rl.question(question, (answer) => {
1193
+ rl.close();
1194
+ resume(Effect.succeed(answer.trim()));
1195
+ });
1196
+ rl.on("error", (error) => {
1197
+ rl.close();
1198
+ resume(Effect.fail(new Error(`Input error: ${error.message}`)));
1199
+ });
1200
+ return Effect.sync(() => rl.close());
1201
+ });
1202
+ var selectPrompt = (question, options) => Effect.gen(function* () {
1203
+ yield* Console.log(question);
1204
+ options.forEach((opt, i) => {
1205
+ console.log(` ${i + 1}) ${opt.label}`);
1206
+ });
1207
+ const answer = yield* prompt("Enter number: ");
1208
+ const index = parseInt(answer, 10) - 1;
1209
+ if (isNaN(index) || index < 0 || index >= options.length) {
1210
+ yield* Console.log(`Invalid selection. Please enter 1-${options.length}.`);
1211
+ return yield* selectPrompt(question, options);
1212
+ }
1213
+ return options[index].value;
1214
+ });
1215
+ var confirmPrompt = (question, defaultValue) => Effect.gen(function* () {
1216
+ const hint = "[y/N]";
1217
+ const answer = yield* prompt(`${question} ${hint} `);
1218
+ if (answer === "") return defaultValue;
1219
+ const lower = answer.toLowerCase();
1220
+ if (lower === "y" || lower === "yes") return true;
1221
+ if (lower === "n" || lower === "no") return false;
1222
+ yield* Console.log("Please answer 'y' or 'n'.");
1223
+ return yield* confirmPrompt(question, defaultValue);
1224
+ });
1225
+ var runInteractive = () => Effect.gen(function* () {
1226
+ yield* Console.log("");
1227
+ yield* Console.log("Create a new Effect GraphQL project");
1228
+ yield* Console.log("====================================");
1229
+ yield* Console.log("");
1230
+ const name = yield* prompt("Project name: ");
1231
+ if (!name) {
1232
+ return yield* Effect.fail(new Error("Project name is required"));
1233
+ }
1234
+ if (!/^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name)) {
1235
+ yield* Console.log("Warning: Name may not be a valid npm package name.");
1236
+ }
1237
+ const serverType = yield* selectPrompt("Select server type:", [
1238
+ { label: "Node.js (@effect-gql/node)", value: "node" },
1239
+ { label: "Bun (@effect-gql/bun)", value: "bun" },
1240
+ { label: "Express (@effect-gql/express)", value: "express" },
1241
+ { label: "Web/Workers (@effect-gql/web)", value: "web" }
1242
+ ]);
1243
+ const monorepo = yield* confirmPrompt("Create as monorepo workspace package?", false);
1244
+ const packageManager = monorepo ? Option.some(
1245
+ yield* selectPrompt("Select package manager:", [
1246
+ { label: "pnpm", value: "pnpm" },
1247
+ { label: "npm", value: "npm" },
1248
+ { label: "yarn", value: "yarn" },
1249
+ { label: "bun", value: "bun" }
1250
+ ])
1251
+ ) : Option.none();
1252
+ return {
1253
+ name,
1254
+ serverType,
1255
+ directory: Option.none(),
1256
+ monorepo: Option.some(monorepo),
1257
+ skipInstall: false,
1258
+ packageManager
1259
+ };
1260
+ });
1261
+ var runCreate = (args) => Effect.gen(function* () {
1262
+ const parsed = parseArgs2(args);
1263
+ if ("help" in parsed) {
1264
+ printCreateHelp();
1265
+ return;
1266
+ }
1267
+ if ("error" in parsed) {
1268
+ yield* Console.error(`Error: ${parsed.error}`);
1269
+ yield* Console.log("");
1270
+ printCreateHelp();
1271
+ process.exitCode = 1;
1272
+ return;
1273
+ }
1274
+ const options = "interactive" in parsed ? yield* runInteractive() : parsed.options;
1275
+ yield* scaffold(options);
1276
+ });
1277
+
159
1278
  // src/bin.ts
160
1279
  var VERSION = "0.1.0";
161
1280
  var printHelp = () => {
@@ -166,7 +1285,7 @@ Usage: effect-gql <command> [options]
166
1285
 
167
1286
  Commands:
168
1287
  generate-schema Generate GraphQL SDL from a schema module
169
- create Create a new Effect GraphQL project (coming soon)
1288
+ create Create a new Effect GraphQL project
170
1289
 
171
1290
  Options:
172
1291
  -h, --help Show this help message
@@ -175,7 +1294,7 @@ Options:
175
1294
  Examples:
176
1295
  effect-gql generate-schema ./src/schema.ts
177
1296
  effect-gql generate-schema ./src/schema.ts -o schema.graphql
178
- effect-gql create my-app
1297
+ effect-gql create my-app --server-type node
179
1298
 
180
1299
  Run 'effect-gql <command> --help' for command-specific help.
181
1300
  `);
@@ -200,8 +1319,7 @@ var main = Effect.gen(function* () {
200
1319
  yield* runGenerateSchema(commandArgs);
201
1320
  break;
202
1321
  case "create":
203
- yield* Console.log("The 'create' command is coming soon!");
204
- yield* Console.log("It will help you scaffold new Effect GraphQL projects.");
1322
+ yield* runCreate(commandArgs);
205
1323
  break;
206
1324
  default:
207
1325
  yield* Console.error(`Unknown command: ${command}`);