polen 0.11.0-next.22 → 0.11.0-next.24

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.
Files changed (93) hide show
  1. package/build/api/config/normalized.d.ts +200 -200
  2. package/build/api/examples/schemas/catalog.d.ts +4 -4
  3. package/build/api/examples/schemas/type-usage-index.d.ts +5 -5
  4. package/build/api/examples/schemas/type-usage-index.js +2 -2
  5. package/build/api/examples/schemas/type-usage-index.js.map +1 -1
  6. package/build/api/examples/type-usage-indexer.d.ts.map +1 -1
  7. package/build/api/examples/type-usage-indexer.js +41 -35
  8. package/build/api/examples/type-usage-indexer.js.map +1 -1
  9. package/build/api/schema/input-sources/directory.d.ts +1 -1
  10. package/build/api/schema/input-sources/directory.js +3 -3
  11. package/build/api/schema/input-sources/directory.js.map +1 -1
  12. package/build/api/schema/input-sources/file.d.ts +1 -1
  13. package/build/api/schema/input-sources/file.js +3 -3
  14. package/build/api/schema/input-sources/file.js.map +1 -1
  15. package/build/api/schema/input-sources/introspection-file.d.ts +1 -1
  16. package/build/api/schema/input-sources/introspection.d.ts +1 -1
  17. package/build/api/schema/input-sources/memory.d.ts +1 -1
  18. package/build/api/schema/input-sources/memory.js +3 -3
  19. package/build/api/schema/input-sources/memory.js.map +1 -1
  20. package/build/api/schema/input-sources/versioned-directory.d.ts +2 -2
  21. package/build/api/schema/input-sources/versioned-directory.js +3 -3
  22. package/build/api/schema/input-sources/versioned-directory.js.map +1 -1
  23. package/build/cli/commands/open.js +1 -1
  24. package/build/cli/commands/open.js.map +1 -1
  25. package/build/lib/catalog/catalog.d.ts +30 -30
  26. package/build/lib/catalog/unversioned.d.ts +8 -8
  27. package/build/lib/catalog/versioned.d.ts +16 -16
  28. package/build/lib/change/change.d.ts +6 -6
  29. package/build/lib/grafaid/$$.d.ts +2 -0
  30. package/build/lib/grafaid/$$.d.ts.map +1 -1
  31. package/build/lib/grafaid/$$.js +2 -0
  32. package/build/lib/grafaid/$$.js.map +1 -1
  33. package/build/lib/grafaid/parse-error.d.ts +46 -0
  34. package/build/lib/grafaid/parse-error.d.ts.map +1 -0
  35. package/build/lib/grafaid/parse-error.js +29 -0
  36. package/build/lib/grafaid/parse-error.js.map +1 -0
  37. package/build/lib/grafaid/parse.d.ts +70 -0
  38. package/build/lib/grafaid/parse.d.ts.map +1 -0
  39. package/build/lib/grafaid/parse.js +119 -0
  40. package/build/lib/grafaid/parse.js.map +1 -0
  41. package/build/lib/grafaid/schema/ast.d.ts +0 -2
  42. package/build/lib/grafaid/schema/ast.d.ts.map +1 -1
  43. package/build/lib/grafaid/schema/ast.js +1 -7
  44. package/build/lib/grafaid/schema/ast.js.map +1 -1
  45. package/build/lib/grafaid/schema/read.js +2 -2
  46. package/build/lib/grafaid/schema/read.js.map +1 -1
  47. package/build/lib/grafaid/schema/schema.d.ts +2 -1
  48. package/build/lib/grafaid/schema/schema.d.ts.map +1 -1
  49. package/build/lib/grafaid/schema/schema.js +5 -1
  50. package/build/lib/grafaid/schema/schema.js.map +1 -1
  51. package/build/lib/graphql-schema-loader/graphql-schema-loader.d.ts.map +1 -1
  52. package/build/lib/graphql-schema-loader/graphql-schema-loader.js +4 -4
  53. package/build/lib/graphql-schema-loader/graphql-schema-loader.js.map +1 -1
  54. package/build/lib/revision/revision.d.ts +30 -30
  55. package/build/lib/schema/schema.d.ts +18 -18
  56. package/build/lib/schema/unversioned.d.ts +28 -28
  57. package/build/lib/schema/versioned.d.ts +16 -16
  58. package/build/template/components/ExampleLink.d.ts.map +1 -1
  59. package/build/template/components/ExampleLink.js +4 -2
  60. package/build/template/components/ExampleLink.js.map +1 -1
  61. package/build/template/components/NamedType.d.ts.map +1 -1
  62. package/build/template/components/NamedType.js +1 -1
  63. package/build/template/components/NamedType.js.map +1 -1
  64. package/build/template/components/VersionCoveragePicker.d.ts.map +1 -1
  65. package/build/template/components/VersionCoveragePicker.js +6 -1
  66. package/build/template/components/VersionCoveragePicker.js.map +1 -1
  67. package/build/template/routes/pages.d.ts.map +1 -1
  68. package/build/template/routes/pages.js +5 -1
  69. package/build/template/routes/pages.js.map +1 -1
  70. package/build/template/stores/changelog.d.ts +1 -1
  71. package/package.json +1 -1
  72. package/src/api/examples/schemas/type-usage-index.ts +2 -2
  73. package/src/api/examples/type-usage-indexer.test.ts +152 -234
  74. package/src/api/examples/type-usage-indexer.ts +64 -52
  75. package/src/api/schema/$.test.ts +1 -1
  76. package/src/api/schema/input-sources/directory.ts +3 -3
  77. package/src/api/schema/input-sources/file.ts +3 -3
  78. package/src/api/schema/input-sources/memory.ts +3 -3
  79. package/src/api/schema/input-sources/versioned-directory.ts +3 -3
  80. package/src/cli/commands/open.ts +1 -1
  81. package/src/lib/grafaid/$$.ts +2 -0
  82. package/src/lib/grafaid/parse-error.ts +69 -0
  83. package/src/lib/grafaid/parse.test.ts +175 -0
  84. package/src/lib/grafaid/parse.ts +165 -0
  85. package/src/lib/grafaid/schema/ast.ts +1 -9
  86. package/src/lib/grafaid/schema/read.ts +2 -2
  87. package/src/lib/grafaid/schema/schema.ts +10 -2
  88. package/src/lib/graphql-schema-loader/graphql-schema-loader.ts +4 -12
  89. package/src/lib/path-map/$.test.ts +28 -13
  90. package/src/template/components/ExampleLink.tsx +4 -2
  91. package/src/template/components/NamedType.tsx +3 -1
  92. package/src/template/components/VersionCoveragePicker.tsx +8 -2
  93. package/src/template/routes/pages.tsx +6 -1
@@ -121,11 +121,11 @@ export const normalize = (configInput: Options): Config => {
121
121
  const parseSchema = (value: string | GraphQLSchema): Effect.Effect<GraphQLSchema, InputSource.InputSourceError> =>
122
122
  Effect.gen(function*() {
123
123
  if (typeof value === 'string') {
124
- const ast = yield* Grafaid.Schema.AST.parse(value).pipe(
124
+ const ast = yield* Grafaid.Parse.parseSchema(value, { source: 'memory' }).pipe(
125
125
  Effect.mapError((error) =>
126
126
  new InputSource.InputSourceError({
127
127
  source: 'memory',
128
- message: `Failed to parse schema: ${error}`,
128
+ message: error.message,
129
129
  cause: error,
130
130
  })
131
131
  ),
@@ -134,7 +134,7 @@ const parseSchema = (value: string | GraphQLSchema): Effect.Effect<GraphQLSchema
134
134
  Effect.mapError((error) =>
135
135
  new InputSource.InputSourceError({
136
136
  source: 'memory',
137
- message: `Failed to build schema: ${error}`,
137
+ message: error.message,
138
138
  cause: error,
139
139
  })
140
140
  ),
@@ -201,11 +201,11 @@ export const readOrThrow = (
201
201
  Effect.gen(function*() {
202
202
  const filePath = Path.join(versionInfo.path, revisionFile)
203
203
  const content = yield* fs.readFileString(filePath)
204
- const ast = yield* Grafaid.Schema.AST.parse(content).pipe(
204
+ const ast = yield* Grafaid.Parse.parseSchema(content, { source: filePath }).pipe(
205
205
  Effect.mapError((error) =>
206
206
  new InputSource.InputSourceError({
207
207
  source: 'versionedDirectory',
208
- message: `Failed to parse schema from ${filePath}: ${error}`,
208
+ message: error.message,
209
209
  cause: error,
210
210
  })
211
211
  ),
@@ -214,7 +214,7 @@ export const readOrThrow = (
214
214
  Effect.mapError((error) =>
215
215
  new InputSource.InputSourceError({
216
216
  source: 'versionedDirectory',
217
- message: `Failed to build schema from ${filePath}: ${error}`,
217
+ message: error.message,
218
218
  cause: error,
219
219
  })
220
220
  ),
@@ -74,7 +74,7 @@ const cacheRead = async (source: string, useCache: boolean) => {
74
74
  const filePath = Path.join(cacheDir, fileName)
75
75
  const sdl = await Fs.read(filePath)
76
76
  if (!sdl) return null
77
- const documentNode = await Effect.runPromise(Grafaid.Schema.AST.parse(sdl))
77
+ const documentNode = await Effect.runPromise(Grafaid.Parse.parseSchema(sdl, { source: filePath }))
78
78
  return await Effect.runPromise(Grafaid.Schema.fromAST(documentNode))
79
79
  })
80
80
  }
@@ -1,4 +1,6 @@
1
1
  export * as Document from './document.js'
2
2
  export * from './graphql.js'
3
3
  export * as HTTP from './http/http.js'
4
+ export { ParseError } from './parse-error.js'
5
+ export * as Parse from './parse.js'
4
6
  export * as Schema from './schema/schema.js'
@@ -0,0 +1,69 @@
1
+ import { Data } from 'effect'
2
+
3
+ /**
4
+ * Error that occurs when parsing GraphQL source text fails.
5
+ *
6
+ * This error provides structured information about parse failures,
7
+ * including the source context and the underlying parse error details.
8
+ */
9
+ export class ParseError extends Data.TaggedError('ParseError')<{
10
+ /**
11
+ * The type of GraphQL content being parsed.
12
+ * Helps identify whether the error occurred while parsing a schema or a document.
13
+ */
14
+ readonly parseType: 'schema' | 'document' | 'unknown'
15
+
16
+ /**
17
+ * Optional source identifier for debugging.
18
+ * Could be a file path, URL, or other identifier.
19
+ */
20
+ readonly source?: string
21
+
22
+ /**
23
+ * The error message describing what went wrong.
24
+ */
25
+ readonly message: string
26
+
27
+ /**
28
+ * The original error that caused the parse failure.
29
+ */
30
+ readonly cause?: unknown
31
+
32
+ /**
33
+ * Optional excerpt of the source text where the error occurred.
34
+ * Useful for showing context in error messages.
35
+ */
36
+ readonly excerpt?: string
37
+ }> {}
38
+
39
+ /**
40
+ * Helper to create a ParseError with common defaults.
41
+ */
42
+ export const makeParseError = (
43
+ message: string,
44
+ options?: {
45
+ parseType?: 'schema' | 'document' | 'unknown'
46
+ source?: string
47
+ cause?: unknown
48
+ excerpt?: string
49
+ },
50
+ ): ParseError => {
51
+ const props: any = {
52
+ parseType: options?.parseType ?? 'unknown',
53
+ message,
54
+ }
55
+
56
+ if (options?.source !== undefined) {
57
+ props.source = options.source
58
+ }
59
+
60
+ if (options?.cause !== undefined) {
61
+ props.cause = options.cause
62
+ }
63
+
64
+ if (options?.excerpt !== undefined) {
65
+ props.excerpt = options.excerpt
66
+ }
67
+
68
+ return new ParseError(props)
69
+ }
@@ -0,0 +1,175 @@
1
+ import { Cause, Effect, Exit, Option } from 'effect'
2
+ import { expect, test } from 'vitest'
3
+ import { Grafaid } from './$.js'
4
+
5
+ test('parseSchema - parses valid GraphQL schema SDL', () => {
6
+ const sdl = `type Query { hello: String }`
7
+ const result = Effect.runSync(Grafaid.Parse.parseSchema(sdl))
8
+ expect(result).toBeDefined()
9
+ expect(result.kind).toBe('Document')
10
+ expect(result.definitions).toHaveLength(1)
11
+ })
12
+
13
+ test('parseSchema - returns ParseError for invalid schema SDL', () => {
14
+ const invalidSdl = `type Query { hello: String missing_closing_brace`
15
+ const result = Effect.runSyncExit(Grafaid.Parse.parseSchema(invalidSdl, { source: 'test.graphql' }))
16
+ expect(Exit.isFailure(result)).toBe(true)
17
+
18
+ if (Exit.isFailure(result)) {
19
+ const error = Cause.failureOption(result.cause)
20
+ expect(Option.isSome(error)).toBe(true)
21
+ if (Option.isSome(error)) {
22
+ expect(error.value).toBeInstanceOf(Grafaid.ParseError)
23
+ expect(error.value._tag).toBe('ParseError')
24
+ expect(error.value.parseType).toBe('schema')
25
+ expect(error.value.source).toBe('test.graphql')
26
+ expect(error.value.message).toContain('Failed to parse GraphQL schema')
27
+ expect(error.value.message).toContain('test.graphql')
28
+ }
29
+ }
30
+ })
31
+
32
+ test('parseSchema - includes excerpt in error for parse failures', () => {
33
+ const invalidSdl = `type Query {
34
+ hello: String
35
+ invalid syntax here
36
+ }`
37
+ const result = Effect.runSyncExit(Grafaid.Parse.parseSchema(invalidSdl))
38
+ expect(Exit.isFailure(result)).toBe(true)
39
+
40
+ if (Exit.isFailure(result)) {
41
+ const error = Cause.failureOption(result.cause)
42
+ expect(Option.isSome(error)).toBe(true)
43
+ if (Option.isSome(error)) {
44
+ expect(error.value.excerpt).toBeDefined()
45
+ expect(error.value.excerpt).toContain('Line')
46
+ }
47
+ }
48
+ })
49
+
50
+ test('parseDocument - parses valid GraphQL query document', () => {
51
+ const query = `query GetUser { user { id name } }`
52
+ const result = Effect.runSync(Grafaid.Parse.parseDocument(query))
53
+ expect(result).toBeDefined()
54
+ expect(result.kind).toBe('Document')
55
+ expect(result.definitions).toHaveLength(1)
56
+ expect(result.definitions[0]?.kind).toBe('OperationDefinition')
57
+ })
58
+
59
+ test('parseDocument - parses anonymous query', () => {
60
+ const query = `{ user { id name } }`
61
+ const result = Effect.runSync(Grafaid.Parse.parseDocument(query))
62
+ expect(result).toBeDefined()
63
+ expect(result.kind).toBe('Document')
64
+ expect(result.definitions).toHaveLength(1)
65
+ })
66
+
67
+ test('parseDocument - returns ParseError for invalid query document', () => {
68
+ const invalidQuery = `query GetUser { user { id name // invalid comment syntax } }`
69
+ const result = Effect.runSyncExit(Grafaid.Parse.parseDocument(invalidQuery, { source: 'getUserQuery' }))
70
+ expect(Exit.isFailure(result)).toBe(true)
71
+
72
+ if (Exit.isFailure(result)) {
73
+ const error = Cause.failureOption(result.cause)
74
+ expect(Option.isSome(error)).toBe(true)
75
+ if (Option.isSome(error)) {
76
+ expect(error.value).toBeInstanceOf(Grafaid.ParseError)
77
+ expect(error.value._tag).toBe('ParseError')
78
+ expect(error.value.parseType).toBe('document')
79
+ expect(error.value.source).toBe('getUserQuery')
80
+ expect(error.value.message).toContain('Failed to parse GraphQL document')
81
+ }
82
+ }
83
+ })
84
+
85
+ test('parse - auto-detects schema type', () => {
86
+ const sdl = `type Query { hello: String }`
87
+ const result = Effect.runSyncExit(Grafaid.Parse.parse(sdl))
88
+ expect(Exit.isSuccess(result)).toBe(true)
89
+
90
+ // Check error for parseType inference
91
+ const invalidSdl = `type Query { invalid`
92
+ const errorResult = Effect.runSyncExit(Grafaid.Parse.parse(invalidSdl))
93
+
94
+ if (Exit.isFailure(errorResult)) {
95
+ const error = Cause.failureOption(errorResult.cause)
96
+ if (Option.isSome(error)) {
97
+ expect(error.value.parseType).toBe('schema')
98
+ }
99
+ }
100
+ })
101
+
102
+ test('parse - auto-detects document type for query', () => {
103
+ const query = `query GetUser { user { id } }`
104
+ const result = Effect.runSyncExit(Grafaid.Parse.parse(query))
105
+ expect(Exit.isSuccess(result)).toBe(true)
106
+
107
+ // Check error for parseType inference
108
+ const invalidQuery = `query GetUser { invalid`
109
+ const errorResult = Effect.runSyncExit(Grafaid.Parse.parse(invalidQuery))
110
+
111
+ if (Exit.isFailure(errorResult)) {
112
+ const error = Cause.failureOption(errorResult.cause)
113
+ if (Option.isSome(error)) {
114
+ expect(error.value.parseType).toBe('document')
115
+ }
116
+ }
117
+ })
118
+
119
+ test('parse - auto-detects document type for anonymous query', () => {
120
+ const query = `{ user { id } }`
121
+ const result = Effect.runSyncExit(Grafaid.Parse.parse(query))
122
+ expect(Exit.isSuccess(result)).toBe(true)
123
+
124
+ // Check error for parseType inference
125
+ const invalidQuery = `{ invalid`
126
+ const errorResult = Effect.runSyncExit(Grafaid.Parse.parse(invalidQuery))
127
+
128
+ if (Exit.isFailure(errorResult)) {
129
+ const error = Cause.failureOption(errorResult.cause)
130
+ if (Option.isSome(error)) {
131
+ expect(error.value.parseType).toBe('document')
132
+ }
133
+ }
134
+ })
135
+
136
+ test('parse - returns unknown type when unable to infer', () => {
137
+ const ambiguous = `something that is not clearly schema or document`
138
+ const errorResult = Effect.runSyncExit(Grafaid.Parse.parse(ambiguous))
139
+
140
+ if (Exit.isFailure(errorResult)) {
141
+ const error = Cause.failureOption(errorResult.cause)
142
+ if (Option.isSome(error)) {
143
+ expect(error.value.parseType).toBe('unknown')
144
+ }
145
+ }
146
+ })
147
+
148
+ test.for([
149
+ { input: 'type Query { field: String }', expectedType: 'schema' },
150
+ { input: 'interface Node { id: ID! }', expectedType: 'schema' },
151
+ { input: 'enum Status { ACTIVE }', expectedType: 'schema' },
152
+ { input: 'scalar Date', expectedType: 'schema' },
153
+ { input: 'union Result = Success | Error', expectedType: 'schema' },
154
+ { input: 'input Filter { name: String }', expectedType: 'schema' },
155
+ { input: 'schema { query: Query }', expectedType: 'schema' },
156
+ { input: 'extend type Query { extra: String }', expectedType: 'schema' },
157
+ { input: 'directive @custom on FIELD', expectedType: 'schema' },
158
+ { input: 'query GetData { field }', expectedType: 'document' },
159
+ { input: 'mutation UpdateData { field }', expectedType: 'document' },
160
+ { input: 'subscription OnData { field }', expectedType: 'document' },
161
+ { input: 'fragment UserFields on User { id }', expectedType: 'document' },
162
+ { input: '{ field }', expectedType: 'document' },
163
+ { input: 'random text', expectedType: 'unknown' },
164
+ ])('parse - infers parseType "$expectedType" for "$input"', ({ input, expectedType }) => {
165
+ const errorResult = Effect.runSyncExit(Grafaid.Parse.parse(input))
166
+
167
+ // These will all fail to parse, but we're testing the type inference
168
+ if (Exit.isFailure(errorResult)) {
169
+ const error = Cause.failureOption(errorResult.cause)
170
+ expect(Option.isSome(error)).toBe(true)
171
+ if (Option.isSome(error)) {
172
+ expect(error.value.parseType).toBe(expectedType)
173
+ }
174
+ }
175
+ })
@@ -0,0 +1,165 @@
1
+ import { Effect } from 'effect'
2
+ import { type DocumentNode, Kind, parse as graphqlParse } from 'graphql'
3
+ import { makeParseError, ParseError } from './parse-error.js'
4
+
5
+ /**
6
+ * Parse GraphQL source text into an AST.
7
+ *
8
+ * This is the centralized parsing function that should be used throughout
9
+ * the codebase for parsing any GraphQL content (schemas or documents).
10
+ *
11
+ * @param source - The GraphQL source text to parse
12
+ * @param options - Optional parsing configuration
13
+ * @returns An Effect that yields a DocumentNode AST or fails with ParseError
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // Parse a schema
18
+ * const schemaAst = yield* parse(schemaSDL, {
19
+ * parseType: 'schema',
20
+ * source: 'schema.graphql'
21
+ * })
22
+ *
23
+ * // Parse a query document
24
+ * const queryAst = yield* parse(queryString, {
25
+ * parseType: 'document',
26
+ * source: 'getUserQuery'
27
+ * })
28
+ * ```
29
+ */
30
+ export const parse = (
31
+ source: string,
32
+ options?: {
33
+ /**
34
+ * The type of content being parsed.
35
+ * Helps provide better error messages.
36
+ */
37
+ parseType?: 'schema' | 'document' | 'unknown'
38
+
39
+ /**
40
+ * Optional source identifier for debugging.
41
+ * Could be a file path, URL, or descriptive name.
42
+ */
43
+ source?: string
44
+ },
45
+ ): Effect.Effect<DocumentNode, ParseError> =>
46
+ Effect.try({
47
+ try: () => graphqlParse(source),
48
+ catch: (error) => {
49
+ // Extract useful error information
50
+ let message = 'Failed to parse GraphQL'
51
+ let excerpt: string | undefined
52
+
53
+ if (error instanceof Error) {
54
+ message = error.message
55
+
56
+ // GraphQL parse errors often include location info
57
+ // Try to extract a meaningful excerpt if possible
58
+ if ('locations' in error && Array.isArray(error.locations) && error.locations.length > 0) {
59
+ const location = error.locations[0]
60
+ if (location && typeof location.line === 'number') {
61
+ const lines = source.split('\n')
62
+ const errorLine = lines[location.line - 1]
63
+ if (errorLine) {
64
+ excerpt = `Line ${location.line}: ${errorLine.trim()}`
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ const parseType = options?.parseType ?? inferParseType(source)
71
+
72
+ const errorOptions: Parameters<typeof makeParseError>[1] = {
73
+ parseType,
74
+ cause: error,
75
+ }
76
+
77
+ if (options?.source) {
78
+ errorOptions.source = options.source
79
+ }
80
+
81
+ if (excerpt) {
82
+ errorOptions.excerpt = excerpt
83
+ }
84
+
85
+ return makeParseError(
86
+ `Failed to parse GraphQL ${parseType}${options?.source ? ` from ${options.source}` : ''}: ${message}`,
87
+ errorOptions,
88
+ )
89
+ },
90
+ })
91
+
92
+ /**
93
+ * Parse GraphQL schema SDL with typed errors.
94
+ *
95
+ * This is a specialized version of parse() that's explicitly for schemas.
96
+ *
97
+ * @param source - The GraphQL schema SDL to parse
98
+ * @param options - Optional parsing configuration
99
+ * @returns An Effect that yields a DocumentNode AST or fails with ParseError
100
+ */
101
+ export const parseSchema = (
102
+ source: string,
103
+ options?: {
104
+ source?: string
105
+ },
106
+ ): Effect.Effect<DocumentNode, ParseError> =>
107
+ parse(source, {
108
+ parseType: 'schema',
109
+ ...options,
110
+ })
111
+
112
+ /**
113
+ * Parse GraphQL document (query/mutation/subscription) with typed errors.
114
+ *
115
+ * This is a specialized version of parse() that's explicitly for executable documents.
116
+ *
117
+ * @param source - The GraphQL document to parse
118
+ * @param options - Optional parsing configuration
119
+ * @returns An Effect that yields a DocumentNode AST or fails with ParseError
120
+ */
121
+ export const parseDocument = (
122
+ source: string,
123
+ options?: {
124
+ source?: string
125
+ },
126
+ ): Effect.Effect<DocumentNode, ParseError> =>
127
+ parse(source, {
128
+ parseType: 'document',
129
+ ...options,
130
+ })
131
+
132
+ /**
133
+ * Try to infer whether the source is a schema or document.
134
+ * This is a best-effort heuristic and may not always be accurate.
135
+ */
136
+ const inferParseType = (source: string): 'schema' | 'document' | 'unknown' => {
137
+ // Common schema definition keywords
138
+ const schemaKeywords = /^\s*(type|interface|enum|scalar|union|input|schema|extend|directive)\s+/m
139
+ // Common document keywords
140
+ const documentKeywords = /^\s*(query|mutation|subscription|fragment)\s+/m
141
+
142
+ if (schemaKeywords.test(source)) {
143
+ return 'schema'
144
+ }
145
+
146
+ if (documentKeywords.test(source)) {
147
+ return 'document'
148
+ }
149
+
150
+ // Check for anonymous query syntax { ... }
151
+ if (/^\s*\{/.test(source)) {
152
+ return 'document'
153
+ }
154
+
155
+ return 'unknown'
156
+ }
157
+
158
+ /**
159
+ * Create an empty DocumentNode.
160
+ * Useful for representing an absence of GraphQL content.
161
+ */
162
+ export const empty: DocumentNode = {
163
+ definitions: [],
164
+ kind: Kind.DOCUMENT,
165
+ }
@@ -1,12 +1,4 @@
1
- import { Effect } from 'effect'
2
- import { type DocumentNode, Kind, parse as graphqlParse } from 'graphql'
3
-
4
- // Effect-based version of parse
5
- export const parse = (source: string): Effect.Effect<DocumentNode, Error> =>
6
- Effect.try({
7
- try: () => graphqlParse(source),
8
- catch: (error) => new Error(`Failed to parse GraphQL: ${error}`),
9
- })
1
+ import { type DocumentNode, Kind } from 'graphql'
10
2
 
11
3
  export { type DocumentNode as Document } from 'graphql'
12
4
 
@@ -2,7 +2,7 @@ import { FileSystem } from '@effect/platform/FileSystem'
2
2
  import { Fs } from '@wollybeard/kit'
3
3
  import { Effect } from 'effect'
4
4
  import { type GraphQLSchema } from 'graphql'
5
- import * as AST from './ast.js'
5
+ import * as Parse from '../parse.js'
6
6
  import { fromAST } from './schema.js'
7
7
 
8
8
  export const read = (sdlFilePath: string): Effect.Effect<null | Fs.File<GraphQLSchema>, Error, FileSystem> =>
@@ -19,7 +19,7 @@ export const read = (sdlFilePath: string): Effect.Effect<null | Fs.File<GraphQLS
19
19
  const content = yield* fs.readFileString(sdlFilePath)
20
20
 
21
21
  // Parse and build schema
22
- const node = yield* AST.parse(content)
22
+ const node = yield* Parse.parseSchema(content, { source: sdlFilePath })
23
23
  const schema = yield* fromAST(node)
24
24
 
25
25
  return {
@@ -1,5 +1,6 @@
1
1
  import { Effect } from 'effect'
2
2
  import { buildASTSchema, type GraphQLSchema } from 'graphql'
3
+ import { makeParseError, ParseError } from '../parse-error.js'
3
4
 
4
5
  export {
5
6
  buildClientSchema as fromIntrospectionQuery,
@@ -9,10 +10,17 @@ export {
9
10
  } from 'graphql'
10
11
 
11
12
  // Effect-based version of fromAST
12
- export const fromAST = (ast: AST.Document): Effect.Effect<GraphQLSchema, Error> =>
13
+ export const fromAST = (ast: AST.Document): Effect.Effect<GraphQLSchema, ParseError> =>
13
14
  Effect.try({
14
15
  try: () => buildASTSchema(ast),
15
- catch: (error) => new Error(`Failed to build schema from AST: ${error}`),
16
+ catch: (error) =>
17
+ makeParseError(
18
+ `Failed to build schema from AST: ${error instanceof Error ? error.message : String(error)}`,
19
+ {
20
+ parseType: 'schema',
21
+ cause: error,
22
+ },
23
+ ),
16
24
  })
17
25
 
18
26
  export * as AST from './ast.js'
@@ -59,24 +59,16 @@ export const load = (source: SchemaPointer): Effect.Effect<Grafaid.Schema.Schema
59
59
  catch: (error) => new Error(`Failed to read response text: ${error}`),
60
60
  })
61
61
 
62
- const ast = yield* Grafaid.Schema.AST.parse(sdlContent).pipe(
63
- Effect.mapError((error) => new Error(`Failed to parse SDL from ${source.pathOrUrl}: ${error}`)),
64
- )
65
- return yield* Grafaid.Schema.fromAST(ast).pipe(
66
- Effect.mapError((error) => new Error(`Failed to build schema from ${source.pathOrUrl}: ${error}`)),
67
- )
62
+ const ast = yield* Grafaid.Parse.parseSchema(sdlContent, { source: source.pathOrUrl })
63
+ return yield* Grafaid.Schema.fromAST(ast)
68
64
  } else {
69
65
  const fs = yield* FileSystem
70
66
  const sdlContent = yield* fs.readFileString(source.pathOrUrl).pipe(
71
67
  Effect.mapError((error) => new Error(`Failed to read SDL from ${source.pathOrUrl}: ${error}`)),
72
68
  )
73
69
 
74
- const ast = yield* Grafaid.Schema.AST.parse(sdlContent).pipe(
75
- Effect.mapError((error) => new Error(`Failed to parse SDL from ${source.pathOrUrl}: ${error}`)),
76
- )
77
- return yield* Grafaid.Schema.fromAST(ast).pipe(
78
- Effect.mapError((error) => new Error(`Failed to build schema from ${source.pathOrUrl}: ${error}`)),
79
- )
70
+ const ast = yield* Grafaid.Parse.parseSchema(sdlContent, { source: source.pathOrUrl })
71
+ return yield* Grafaid.Schema.fromAST(ast)
80
72
  }
81
73
  }
82
74
  case `name`: {
@@ -258,22 +258,37 @@ describe('PathMap', () => {
258
258
 
259
259
  it('absolute paths always start with base', () => {
260
260
  fc.assert(
261
- fc.property(pathInput, fc.string({ minLength: 1 }).filter(s => s.startsWith('/')), (input, base) => {
262
- const paths = PathMap.create(input, base)
263
- const normalizedBase = normalizeBasePath(base)
264
-
265
- function checkAbsolute(obj: any) {
266
- for (const [key, value] of Object.entries(obj)) {
267
- if (typeof value === 'string') {
268
- expect(value.startsWith(normalizedBase)).toBe(true)
269
- } else if (typeof value === 'object') {
270
- checkAbsolute(value)
261
+ fc.property(
262
+ pathInput,
263
+ fc.string({ minLength: 1 })
264
+ .filter(s => s.startsWith('/'))
265
+ // Exclude paths with .. segments as Path.join will resolve them
266
+ .filter(s => !s.includes('..')),
267
+ (input, base) => {
268
+ const paths = PathMap.create(input, base)
269
+ const normalizedBase = normalizeBasePath(base)
270
+
271
+ function checkAbsolute(obj: any) {
272
+ for (const [key, value] of Object.entries(obj)) {
273
+ if (typeof value === 'string') {
274
+ // Path.join resolves .. segments, so we need to resolve the base too
275
+ const resolvedBase = Path.resolve(normalizedBase)
276
+ const resolvedValue = Path.resolve(value)
277
+ // Check that the resolved absolute path starts with resolved base
278
+ // or that both resolve to root
279
+ expect(
280
+ resolvedValue.startsWith(resolvedBase)
281
+ || (resolvedBase === '/' && resolvedValue.startsWith('/')),
282
+ ).toBe(true)
283
+ } else if (typeof value === 'object') {
284
+ checkAbsolute(value)
285
+ }
271
286
  }
272
287
  }
273
- }
274
288
 
275
- checkAbsolute(paths.absolute)
276
- }),
289
+ checkAbsolute(paths.absolute)
290
+ },
291
+ ),
277
292
  )
278
293
  })
279
294
 
@@ -12,8 +12,10 @@ export interface Props {
12
12
  * Always includes the version query parameter for consistent behavior.
13
13
  */
14
14
  export const ExampleLink: FC<Props> = ({ exampleRef }) => {
15
- // Always include version parameter for consistency
16
- const href = `/examples/${exampleRef.name}?version=${Version.encodeSync(exampleRef.version)}`
15
+ // Include version parameter only if version exists
16
+ const href = exampleRef.version
17
+ ? `/examples/${exampleRef.name}?version=${Version.encodeSync(exampleRef.version)}`
18
+ : `/examples/${exampleRef.name}`
17
19
 
18
20
  return (
19
21
  <Link href={href} style={{ textDecoration: 'none' }}>
@@ -59,7 +59,9 @@ export const NamedType: FC<Props> = ({ data }) => {
59
59
  <Box style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
60
60
  {[...examples].map((exampleRef) => (
61
61
  <ExampleLink
62
- key={`${exampleRef.name}-${Version.encodeSync(exampleRef.version)}`}
62
+ key={`${exampleRef.name}-${
63
+ exampleRef.version ? Version.encodeSync(exampleRef.version) : 'unversioned'
64
+ }`}
63
65
  exampleRef={exampleRef}
64
66
  />
65
67
  ))}
@@ -26,6 +26,12 @@ export const VersionCoveragePicker: FC<Props> = ({
26
26
  const options = Array.from(HashMap.keys(document.versionDocuments))
27
27
  if (options.length === 0) return null
28
28
 
29
+ // Helper to format labels with proper Version/Versions prefix
30
+ const formatLabel = (versionCoverage: VersionCoverage.VersionCoverage): string => {
31
+ const prefix = VersionCoverage.isSingle(versionCoverage) ? 'Version' : 'Versions'
32
+ return `${prefix} ${VersionCoverage.toLabel(versionCoverage)}`
33
+ }
34
+
29
35
  return (
30
36
  <Select.Root
31
37
  value={VersionCoverage.toLabel(current)}
@@ -39,7 +45,7 @@ export const VersionCoveragePicker: FC<Props> = ({
39
45
  }}
40
46
  >
41
47
  <Select.Trigger>
42
- {VersionCoverage.toLabel(current)}
48
+ {formatLabel(current)}
43
49
  </Select.Trigger>
44
50
  <Select.Content position='popper' sideOffset={5}>
45
51
  {options.map(selection => (
@@ -47,7 +53,7 @@ export const VersionCoveragePicker: FC<Props> = ({
47
53
  key={VersionCoverage.toLabel(selection)}
48
54
  value={VersionCoverage.toLabel(selection)}
49
55
  >
50
- {VersionCoverage.toLabel(selection)}
56
+ {formatLabel(selection)}
51
57
  </Select.Item>
52
58
  ))}
53
59
  </Select.Content>