vscode-apollo 1.20.0 → 2.0.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.
Files changed (108) hide show
  1. package/.circleci/config.yml +27 -18
  2. package/.git-blame-ignore-revs +2 -0
  3. package/.nvmrc +1 -1
  4. package/.vscode/launch.json +9 -4
  5. package/.vscode/tasks.json +58 -16
  6. package/.vscodeignore +12 -1
  7. package/CHANGELOG.md +78 -0
  8. package/CODEOWNERS +4 -0
  9. package/README.md +97 -48
  10. package/graphql.configuration.json +5 -1
  11. package/images/marketplace/apollo-wordmark.png +0 -0
  12. package/jest.config.ts +14 -4
  13. package/jest.e2e.config.js +17 -0
  14. package/package.json +67 -68
  15. package/renovate.json +7 -0
  16. package/sampleWorkspace/clientSchema/apollo.config.cjs +10 -0
  17. package/sampleWorkspace/clientSchema/src/clientSchema.js +16 -0
  18. package/sampleWorkspace/clientSchema/src/test.js +18 -0
  19. package/sampleWorkspace/fixtures/starwarsSchema.graphql +299 -0
  20. package/sampleWorkspace/httpSchema/apollo.config.ts +8 -0
  21. package/sampleWorkspace/httpSchema/src/test.js +9 -0
  22. package/sampleWorkspace/localSchema/apollo.config.js +8 -0
  23. package/sampleWorkspace/localSchema/src/test.js +8 -0
  24. package/sampleWorkspace/localSchemaArray/apollo.config.js +12 -0
  25. package/sampleWorkspace/localSchemaArray/planets.graphql +20 -0
  26. package/sampleWorkspace/localSchemaArray/src/test.js +12 -0
  27. package/sampleWorkspace/sampleWorkspace.code-workspace +20 -0
  28. package/sampleWorkspace/spotifyGraph/apollo.config.mjs +5 -0
  29. package/sampleWorkspace/spotifyGraph/src/test.js +11 -0
  30. package/src/__e2e__/mockServer.js +117 -0
  31. package/src/__e2e__/mocks.js +13094 -0
  32. package/src/__e2e__/run.js +23 -0
  33. package/src/__e2e__/runTests.js +44 -0
  34. package/src/__e2e__/setup.js +1 -0
  35. package/src/__e2e__/vscode-environment.js +16 -0
  36. package/src/__e2e__/vscode.js +1 -0
  37. package/src/build.js +57 -0
  38. package/src/env/index.ts +0 -3
  39. package/src/extension.ts +251 -225
  40. package/src/language-server/__e2e__/clientSchema.e2e.ts +147 -0
  41. package/src/language-server/__e2e__/httpSchema.e2e.ts +21 -0
  42. package/src/language-server/__e2e__/localSchema.e2e.ts +25 -0
  43. package/src/language-server/__e2e__/localSchemaArray.e2e.ts +31 -0
  44. package/src/language-server/__e2e__/studioGraph.e2e.ts +65 -0
  45. package/src/language-server/__e2e__/utils.ts +151 -0
  46. package/src/language-server/__tests__/diagnostics.test.ts +8 -8
  47. package/src/language-server/__tests__/fileSet.test.ts +1 -1
  48. package/src/language-server/__tests__/fixtures/starwarsSchema.ts +2 -2
  49. package/src/language-server/config/__tests__/config.ts +22 -96
  50. package/src/language-server/config/__tests__/loadConfig.ts +97 -221
  51. package/src/language-server/config/__tests__/utils.ts +22 -29
  52. package/src/language-server/config/config.ts +221 -156
  53. package/src/language-server/config/loadConfig.ts +26 -153
  54. package/src/language-server/config/utils.ts +5 -16
  55. package/src/language-server/diagnostics.ts +17 -8
  56. package/src/language-server/document.ts +16 -16
  57. package/src/language-server/engine/index.ts +57 -39
  58. package/src/language-server/engine/operations/frontendUrlRoot.ts +9 -1
  59. package/src/language-server/engine/operations/schemaTagsAndFieldStats.ts +9 -1
  60. package/src/language-server/errors/__tests__/NoMissingClientDirectives.test.ts +10 -5
  61. package/src/language-server/errors/logger.ts +1 -1
  62. package/src/language-server/errors/validation.ts +20 -23
  63. package/src/language-server/fileSet.ts +10 -12
  64. package/src/language-server/format.ts +1 -1
  65. package/src/language-server/graphqlTypes.ts +13020 -3455
  66. package/src/language-server/index.ts +0 -1
  67. package/src/language-server/languageProvider.ts +29 -32
  68. package/src/language-server/loadingHandler.ts +10 -27
  69. package/src/language-server/project/base.ts +32 -25
  70. package/src/language-server/project/client.ts +80 -114
  71. package/src/language-server/project/defaultClientSchema.ts +29 -4
  72. package/src/language-server/providers/schema/__tests__/file.ts +60 -19
  73. package/src/language-server/providers/schema/base.ts +2 -2
  74. package/src/language-server/providers/schema/endpoint.ts +15 -34
  75. package/src/language-server/providers/schema/engine.ts +25 -18
  76. package/src/language-server/providers/schema/file.ts +41 -32
  77. package/src/language-server/providers/schema/index.ts +5 -21
  78. package/src/language-server/server.ts +72 -50
  79. package/src/language-server/typings/graphql.d.ts +3 -3
  80. package/src/language-server/utilities/__tests__/graphql.test.ts +42 -54
  81. package/src/language-server/utilities/debouncer.ts +1 -1
  82. package/src/language-server/utilities/debug.ts +6 -5
  83. package/src/language-server/utilities/graphql.ts +17 -16
  84. package/src/language-server/utilities/source.ts +16 -16
  85. package/src/language-server/utilities/uri.ts +2 -2
  86. package/src/language-server/workspace.ts +29 -37
  87. package/src/languageServerClient.ts +4 -4
  88. package/src/messages.ts +38 -47
  89. package/src/tools/__tests__/buildServiceDefinition.test.ts +2 -2
  90. package/src/tools/buildServiceDefinition.ts +11 -11
  91. package/src/tools/schema/resolveObject.ts +1 -1
  92. package/src/tools/utilities/predicates.ts +1 -1
  93. package/src/utils.ts +7 -6
  94. package/syntaxes/graphql.dart.json +2 -4
  95. package/syntaxes/graphql.ex.json +1 -4
  96. package/tsconfig.build.json +8 -1
  97. package/tsconfig.json +5 -3
  98. package/src/env/fetch/fetch.ts +0 -32
  99. package/src/env/fetch/global.ts +0 -30
  100. package/src/env/fetch/index.d.ts +0 -2
  101. package/src/env/fetch/index.ts +0 -2
  102. package/src/env/fetch/url.ts +0 -9
  103. package/src/env/polyfills/array.ts +0 -17
  104. package/src/env/polyfills/index.ts +0 -2
  105. package/src/env/polyfills/object.ts +0 -7
  106. package/src/language-server/engine/GraphQLDataSource.ts +0 -124
  107. package/src/language-server/project/service.ts +0 -48
  108. package/src/language-server/typings/codemirror.d.ts +0 -4
@@ -1,165 +1,240 @@
1
1
  import { dirname } from "path";
2
- import merge from "lodash.merge";
3
- import { ClientID, ServiceID, ServiceSpecifier } from "../engine";
4
- import URI from "vscode-uri";
5
- import { WithRequired } from "../../env";
2
+ import { URI } from "vscode-uri";
6
3
  import { getGraphIdFromConfig, parseServiceSpecifier } from "./utils";
4
+ import { Debug } from "../utilities";
5
+ import z, { ZodError } from "zod";
7
6
  import { ValidationRule } from "graphql/validation/ValidationContext";
7
+ import { Slot } from "@wry/context";
8
+ import { fromZodError } from "zod-validation-error";
8
9
 
9
- export interface EngineStatsWindow {
10
- to: number;
11
- from: number;
10
+ const ROVER_AVAILABLE = (process.env.APOLLO_FEATURE_FLAGS || "")
11
+ .split(",")
12
+ .includes("rover");
13
+
14
+ function ignoredFieldWarning(
15
+ getMessage = (path: string) =>
16
+ `The option ${path} is no longer supported, please remove it from your configuration file.`,
17
+ ) {
18
+ return z
19
+ .custom<unknown>(() => true)
20
+ .superRefine((val, ctx) => {
21
+ if (val) {
22
+ Debug.warning(getMessage(ctx.path.join(".")));
23
+ }
24
+ })
25
+ .optional();
26
+ }
27
+ export interface Context {
28
+ apiKey?: string;
29
+ serviceName?: string;
12
30
  }
31
+ const context = new Slot<Context>();
13
32
 
14
- export const DefaultEngineStatsWindow = {
15
- to: -0,
16
- from: -86400, // one day
17
- };
33
+ const studioServiceConfig = z.string();
18
34
 
19
- export interface HistoricalEngineStatsWindow extends EngineStatsWindow {}
35
+ const remoteServiceConfig = z.object({
36
+ name: z.string().optional(),
37
+ url: z.string(),
38
+ headers: z.record(z.string()).default({}),
39
+ skipSSLValidation: z.boolean().default(false),
40
+ });
41
+ export type RemoteServiceConfig = z.infer<typeof remoteServiceConfig>;
20
42
 
21
- export type EndpointURI = string;
22
- export interface RemoteServiceConfig {
23
- name: ServiceID;
24
- url: EndpointURI;
25
- headers?: { [key: string]: string };
26
- skipSSLValidation?: boolean;
27
- }
43
+ const localServiceConfig = z.object({
44
+ name: z.string().optional(),
45
+ localSchemaFile: z.union([z.string(), z.array(z.string())]),
46
+ });
47
+ export type LocalServiceConfig = z.infer<typeof localServiceConfig>;
28
48
 
29
- export interface LocalServiceConfig {
30
- name: ServiceID;
31
- localSchemaFile: string | string[];
32
- }
49
+ const clientServiceConfig = z.preprocess(
50
+ (value) => value || context.getValue()?.serviceName,
51
+ z.union([studioServiceConfig, remoteServiceConfig, localServiceConfig]),
52
+ );
53
+ export type ClientServiceConfig = z.infer<typeof clientServiceConfig>;
33
54
 
34
- export interface EngineConfig {
35
- endpoint?: EndpointURI;
36
- readonly apiKey?: string;
37
- }
55
+ const clientConfig = z.object({
56
+ service: clientServiceConfig,
57
+ validationRules: z
58
+ .union([
59
+ z.array(z.custom<ValidationRule>()),
60
+ z.function().args(z.custom<ValidationRule>()).returns(z.boolean()),
61
+ ])
62
+ .optional(),
63
+ // maybe shared with rover?
64
+ includes: z
65
+ .array(z.string())
66
+ .default(["src/**/*.{ts,tsx,js,jsx,graphql,gql}"]),
67
+ // maybe shared with rover?
68
+ excludes: z.array(z.string()).default(["**/node_modules", "**/__tests__"]),
69
+ // maybe shared with rover?
70
+ tagName: z.string().default("gql"),
71
+ // removed:
72
+ clientOnlyDirectives: ignoredFieldWarning(),
73
+ clientSchemaDirectives: ignoredFieldWarning(),
74
+ statsWindow: ignoredFieldWarning(),
75
+ name: ignoredFieldWarning(),
76
+ referenceId: ignoredFieldWarning(),
77
+ version: ignoredFieldWarning(),
78
+ });
79
+ export type ClientConfigFormat = z.infer<typeof clientConfig>;
38
80
 
39
- export const DefaultEngineConfig = {
40
- endpoint: "https://graphql.api.apollographql.com/api/graphql",
41
- };
81
+ const roverConfig = z.object({
82
+ bin: z.string().optional(),
83
+ profile: z.string().optional(),
84
+ });
85
+ type RoverConfigFormat = z.infer<typeof roverConfig>;
42
86
 
43
- export const DefaultConfigBase = {
44
- includes: ["src/**/*.{ts,tsx,js,jsx,graphql,gql}"],
45
- excludes: ["**/node_modules", "**/__tests__"],
46
- };
87
+ const engineConfig = z.object({
88
+ endpoint: z
89
+ .string()
90
+ .default(
91
+ process.env.APOLLO_ENGINE_ENDPOINT ||
92
+ "https://graphql.api.apollographql.com/api/graphql",
93
+ ),
94
+ apiKey: z.preprocess(
95
+ (val) => val || context.getValue()?.apiKey,
96
+ z.string().optional(),
97
+ ),
98
+ });
99
+ export type EngineConfig = z.infer<typeof engineConfig>;
47
100
 
48
- export interface ConfigBase {
49
- includes: string[];
50
- excludes: string[];
51
- }
101
+ const baseConfig = z.object({
102
+ engine: engineConfig.default({}),
103
+ client: z.unknown().optional(),
104
+ rover: z.unknown().optional(),
105
+ service: ignoredFieldWarning(
106
+ (path) =>
107
+ `Service-type projects are no longer supported. Please remove the "${path}" field from your configuration file.`,
108
+ ),
109
+ });
52
110
 
53
- export type ClientServiceConfig = RemoteServiceConfig | LocalServiceConfig;
54
-
55
- export interface ClientConfigFormat extends ConfigBase {
56
- // service linking
57
- service?: ServiceSpecifier | ClientServiceConfig;
58
- // client identity
59
- name?: ClientID;
60
- referenceID?: string;
61
- version?: string;
62
- // client schemas
63
- clientOnlyDirectives?: string[];
64
- clientSchemaDirectives?: string[];
65
- addTypename?: boolean;
66
- tagName?: string;
67
- // stats window config
68
- statsWindow?: EngineStatsWindow;
69
-
70
- /**
71
- * Rules that will be applied when validating GraphQL documents.
72
- *
73
- * If you wish to modify the default list of validation rules, import them from the apollo package and
74
- * assign your custom list:
75
- *
76
- * ```js
77
- * const { defaultValidationRules } = require("apollo/lib/defaultValidationRules");
78
- *
79
- * module.exports = {
80
- * // ...
81
- * validationRules: [...defaultValidationRules, ...customRules]
82
- * }
83
- * ```
84
- *
85
- * Or, if you simply wish to filter out some rules from the default list, you can specify a filter function:
86
- *
87
- * ```js
88
- * module.exports = {
89
- * // ...
90
- * validationRules: rule => rule.name !== "NoAnonymousQueries"
91
- * }
92
- * ```
93
- */
94
- validationRules?: ValidationRule[] | ((rule: ValidationRule) => boolean);
95
- }
111
+ export type FullClientConfigFormat = Extract<
112
+ ParsedApolloConfigFormat,
113
+ { client: {} }
114
+ >;
96
115
 
97
- export const DefaultClientConfig = {
98
- ...DefaultConfigBase,
99
- tagName: "gql",
100
- clientOnlyDirectives: ["connection", "type"],
101
- clientSchemaDirectives: ["client", "rest"],
102
- addTypename: true,
103
- statsWindow: DefaultEngineStatsWindow,
104
- };
105
-
106
- export interface ServiceConfigFormat extends ConfigBase {
107
- name?: string;
108
- endpoint?: Exclude<RemoteServiceConfig, "name">;
109
- localSchemaFile?: string | string[];
110
- }
116
+ export type FullRoverConfigFormat = Extract<
117
+ ParsedApolloConfigFormat,
118
+ { rover: {} }
119
+ >;
111
120
 
112
- export const DefaultServiceConfig = {
113
- ...DefaultConfigBase,
114
- endpoint: {
115
- url: "http://localhost:4000/graphql",
116
- },
117
- };
118
-
119
- export interface ConfigBaseFormat {
120
- client?: ClientConfigFormat;
121
- service?: ServiceConfigFormat;
122
- engine?: EngineConfig;
121
+ /** Helper function for TypeScript - we just want the first type to make it into the types, not the "no feature flag" fallback */
122
+ function ifRoverAvailable<T>(yes: T, no: any): T {
123
+ return ROVER_AVAILABLE ? yes : no;
123
124
  }
124
125
 
125
- export type ApolloConfigFormat =
126
- | WithRequired<ConfigBaseFormat, "client">
127
- | WithRequired<ConfigBaseFormat, "service">;
126
+ export const configSchema = baseConfig
127
+ .superRefine((val, ctx) => {
128
+ if (ROVER_AVAILABLE) {
129
+ if ("client" in val && "rover" in val) {
130
+ ctx.addIssue({
131
+ code: "custom",
132
+ message: "Config cannot contain both 'client' and 'rover' fields",
133
+ fatal: true,
134
+ });
135
+ }
136
+ if (!("client" in val) && !("rover" in val)) {
137
+ ctx.addIssue({
138
+ code: "custom",
139
+ message: "Config needs to contain either 'client' or 'rover' fields",
140
+ fatal: true,
141
+ });
142
+ }
143
+ } else {
144
+ if (!("client" in val)) {
145
+ ctx.addIssue({
146
+ code: "custom",
147
+ message: "Config needs to contain a 'client' field.",
148
+ fatal: true,
149
+ });
150
+ }
151
+ }
152
+ })
153
+ .and(
154
+ ifRoverAvailable(
155
+ z.union([
156
+ z
157
+ .object({
158
+ client: clientConfig,
159
+ })
160
+ .transform((val): typeof val & { rover?: never } => val),
161
+ z
162
+ .object({
163
+ rover: roverConfig,
164
+ })
165
+ .transform((val): typeof val & { client?: never } => val),
166
+ ]),
167
+ z.object({
168
+ client: clientConfig,
169
+ }),
170
+ ),
171
+ );
172
+ export type RawApolloConfigFormat = z.input<typeof configSchema>;
173
+ export type ParsedApolloConfigFormat = z.output<typeof configSchema>;
174
+
175
+ export function parseApolloConfig(
176
+ rawConfig: RawApolloConfigFormat,
177
+ configURI?: URI,
178
+ ctx: Context = {},
179
+ ) {
180
+ const parsed = context.withValue(ctx, () =>
181
+ configSchema.safeParse(rawConfig),
182
+ );
183
+ if (!parsed.success) {
184
+ // Remove "or Required at rover" errors when a client config is provided
185
+ // Remove "or Required at client" errors when a rover config is provided
186
+ for (const [index, error] of parsed.error.errors.entries()) {
187
+ if (error.code === z.ZodIssueCode.invalid_union) {
188
+ error.unionErrors = error.unionErrors.filter((e) => {
189
+ return !(
190
+ e instanceof ZodError &&
191
+ e.errors.length === 1 &&
192
+ e.errors[0].message === "Required" &&
193
+ e.errors[0].path.length === 1
194
+ );
195
+ });
196
+ }
197
+ }
198
+ throw fromZodError(parsed.error, {
199
+ prefix: `Error parsing config file ${configURI?.fsPath}:`,
200
+ prefixSeparator: "\n",
201
+ issueSeparator: ";\n",
202
+ unionSeparator: "\n or\n",
203
+ });
204
+ }
205
+ if (parsed.data.client) {
206
+ return new ClientConfig(parsed.data, configURI);
207
+ } else if (parsed.data.rover) {
208
+ return new RoverConfig(parsed.data, configURI);
209
+ } else {
210
+ // should never happen
211
+ throw new Error("Invalid config file format!");
212
+ }
213
+ }
128
214
 
129
- export class ApolloConfig {
130
- public isClient: boolean;
131
- public isService: boolean;
215
+ export abstract class ApolloConfig {
132
216
  public engine: EngineConfig;
133
- public service?: ServiceConfigFormat;
134
217
  public client?: ClientConfigFormat;
218
+ public rover?: RoverConfigFormat;
135
219
  private _variant?: string;
136
220
  private _graphId?: string;
137
221
 
138
- constructor(public rawConfig: ApolloConfigFormat, public configURI?: URI) {
139
- this.isService = !!rawConfig.service;
140
- this.isClient = !!rawConfig.client;
222
+ protected constructor(
223
+ public rawConfig: ParsedApolloConfigFormat,
224
+ public configURI?: URI,
225
+ ) {
141
226
  this.engine = rawConfig.engine!;
142
227
  this._graphId = getGraphIdFromConfig(rawConfig);
143
- this.client = rawConfig.client;
144
- this.service = rawConfig.service;
145
228
  }
146
229
 
147
230
  get configDirURI() {
148
231
  // if the filepath has a _file_ in it, then we get its dir
149
- return this.configURI && this.configURI.fsPath.match(/\.(ts|js|cjs|json)$/i)
232
+ return this.configURI &&
233
+ this.configURI.fsPath.match(/\.(ts|js|cjs|mjs|json)$/i)
150
234
  ? URI.parse(dirname(this.configURI.fsPath))
151
235
  : this.configURI;
152
236
  }
153
237
 
154
- get projects(): (ClientConfig | ServiceConfig)[] {
155
- const configs = [];
156
- const { client, service } = this.rawConfig;
157
- if (client) configs.push(new ClientConfig(this.rawConfig, this.configURI));
158
- if (service)
159
- configs.push(new ServiceConfig(this.rawConfig, this.configURI));
160
- return configs;
161
- }
162
-
163
238
  set variant(variant: string) {
164
239
  this._variant = variant;
165
240
  }
@@ -170,9 +245,6 @@ export class ApolloConfig {
170
245
  if (this.client && typeof this.client.service === "string") {
171
246
  const parsedVariant = parseServiceSpecifier(this.client.service)[1];
172
247
  if (parsedVariant) tag = parsedVariant;
173
- } else if (this.service && typeof this.service.name === "string") {
174
- const parsedVariant = parseServiceSpecifier(this.service.name)[1];
175
- if (parsedVariant) tag = parsedVariant;
176
248
  }
177
249
  return tag;
178
250
  }
@@ -185,35 +257,28 @@ export class ApolloConfig {
185
257
  if (this._graphId) return this._graphId;
186
258
  return getGraphIdFromConfig(this.rawConfig);
187
259
  }
188
-
189
- // this type needs to be an "EveryKeyIsOptionalApolloConfig"
190
- public setDefaults({
191
- engine,
192
- client,
193
- service,
194
- }: {
195
- engine?: EngineConfig;
196
- client?: ClientConfigFormat;
197
- service?: ServiceConfigFormat;
198
- }): void {
199
- const config = merge(this.rawConfig, { client, engine, service });
200
- this.rawConfig = config;
201
- this.client = config.client;
202
- this.service = config.service;
203
- if (config.engine) {
204
- this.engine = config.engine;
205
- }
206
- }
207
260
  }
208
261
 
209
262
  export class ClientConfig extends ApolloConfig {
210
263
  public client!: ClientConfigFormat;
211
- public isClient!: true;
212
- public isService!: false;
264
+
265
+ constructor(
266
+ public rawConfig: FullClientConfigFormat,
267
+ public configURI?: URI,
268
+ ) {
269
+ super(rawConfig, configURI);
270
+ this.client = rawConfig.client;
271
+ }
213
272
  }
214
273
 
215
- export class ServiceConfig extends ApolloConfig {
216
- public service!: ServiceConfigFormat;
217
- public isClient!: false;
218
- public isService!: true;
274
+ export class RoverConfig extends ApolloConfig {
275
+ public rover!: RoverConfigFormat;
276
+
277
+ constructor(
278
+ public rawConfig: FullRoverConfigFormat,
279
+ public configURI?: URI,
280
+ ) {
281
+ super(rawConfig, configURI);
282
+ this.rover = rawConfig.rover;
283
+ }
219
284
  }
@@ -1,19 +1,13 @@
1
- import cosmiconfig from "cosmiconfig";
2
- import { LoaderEntry } from "cosmiconfig";
3
- import TypeScriptLoader from "@endemolshinegroup/cosmiconfig-typescript-loader";
1
+ import { cosmiconfig } from "cosmiconfig";
4
2
  import { resolve } from "path";
5
3
  import { readFileSync, existsSync, lstatSync } from "fs";
6
- import merge from "lodash.merge";
7
4
  import {
8
5
  ApolloConfig,
9
- ApolloConfigFormat,
10
- DefaultConfigBase,
11
- DefaultClientConfig,
12
- DefaultServiceConfig,
13
- DefaultEngineConfig,
6
+ RawApolloConfigFormat,
7
+ parseApolloConfig,
14
8
  } from "./config";
15
9
  import { getServiceFromKey } from "./utils";
16
- import URI from "vscode-uri";
10
+ import { URI } from "vscode-uri";
17
11
  import { Debug } from "../utilities";
18
12
 
19
13
  // config settings
@@ -22,207 +16,86 @@ const defaultFileNames = [
22
16
  "package.json",
23
17
  `${MODULE_NAME}.config.js`,
24
18
  `${MODULE_NAME}.config.ts`,
19
+ `${MODULE_NAME}.config.mjs`,
25
20
  `${MODULE_NAME}.config.cjs`,
26
21
  ];
27
22
  const envFileNames = [".env", ".env.local"];
28
23
 
29
- const loaders = {
30
- // XXX improve types for config
31
- ".json": (cosmiconfig as any).loadJson as LoaderEntry,
32
- ".js": (cosmiconfig as any).loadJs as LoaderEntry,
33
- ".cjs": (cosmiconfig as any).loadJs as LoaderEntry,
34
- ".ts": {
35
- async: TypeScriptLoader,
36
- },
37
- };
38
-
39
- export const legacyKeyEnvVar = "ENGINE_API_KEY";
40
24
  export const keyEnvVar = "APOLLO_KEY";
41
25
 
42
26
  export interface LoadConfigSettings {
43
27
  // the current working directory to start looking for the config
44
28
  // config loading only works on node so we default to
45
29
  // process.cwd()
46
-
47
- // configPath and fileName are used in conjunction with one another.
48
- // i.e. /User/myProj/my.config.js
49
- // => { configPath: '/User/myProj/', configFileName: 'my.config.js' }
50
- configPath?: string;
51
-
52
- // if a configFileName is passed in, loadConfig won't accept any other
53
- // configs as a fallback.
54
- configFileName?: string;
55
-
56
- // used when run by a `Workspace` where we _know_ a config file should be present.
57
- requireConfig?: boolean;
58
-
59
- // for CLI usage, we don't _require_ a config file for everything. This allows us to pass in
60
- // options to build one at runtime
61
- name?: string;
62
- type?: "service" | "client";
30
+ configPath: string;
63
31
  }
64
32
 
65
33
  export type ConfigResult<T> = {
66
34
  config: T;
67
35
  filepath: string;
36
+ isEmpty?: boolean;
68
37
  } | null;
69
38
 
70
39
  // XXX load .env files automatically
71
40
  export async function loadConfig({
72
41
  configPath,
73
- configFileName,
74
- requireConfig = false,
75
- name,
76
- type,
77
42
  }: LoadConfigSettings): Promise<ApolloConfig | null> {
78
43
  const explorer = cosmiconfig(MODULE_NAME, {
79
- searchPlaces: configFileName ? [configFileName] : defaultFileNames,
80
- loaders,
44
+ searchPlaces: defaultFileNames,
81
45
  });
82
46
 
83
47
  // search can fail if a file can't be parsed (ex: a nonsense js file) so we wrap in a try/catch
84
- let loadedConfig;
48
+ let loadedConfig: ConfigResult<RawApolloConfigFormat>;
85
49
  try {
86
50
  loadedConfig = (await explorer.search(
87
- configPath
88
- )) as ConfigResult<ApolloConfigFormat>;
51
+ configPath,
52
+ )) as ConfigResult<RawApolloConfigFormat>;
89
53
  } catch (error) {
90
- Debug.error(`A config file failed to load with options: ${JSON.stringify(
91
- arguments[0]
54
+ throw new Error(`A config file failed to load with options: ${JSON.stringify(
55
+ arguments[0],
92
56
  )}.
93
57
  The error was: ${error}`);
94
- return null;
95
58
  }
96
59
 
97
- if (configPath && !loadedConfig) {
60
+ if (!loadedConfig || loadedConfig.isEmpty) {
98
61
  Debug.error(
99
- `A config file failed to load at '${configPath}'. This is likely because this file is empty or malformed. For more information, please refer to: https://go.apollo.dev/t/config`
62
+ `No Apollo config found for project or config file failed to load. For more information, please refer to: https://go.apollo.dev/t/config`,
100
63
  );
64
+ // deliberately returning `null` here, but not throwing an error - the user may not have a config file and that's okay, it might just be a project without a graph
101
65
  return null;
102
66
  }
103
67
 
104
- if (loadedConfig && loadedConfig.filepath.endsWith("package.json")) {
68
+ if (loadedConfig.filepath.endsWith("package.json")) {
105
69
  Debug.warning(
106
- 'The "apollo" package.json configuration key will no longer be supported in Apollo v3. Please use the apollo.config.js file for Apollo project configuration. For more information, see: https://go.apollo.dev/t/config'
70
+ 'The "apollo" package.json configuration key will no longer be supported in Apollo v3. Please use the apollo.config.js file for Apollo project configuration. For more information, see: https://go.apollo.dev/t/config',
107
71
  );
108
72
  }
109
73
 
110
- if (requireConfig && !loadedConfig) {
111
- Debug.error(
112
- `No Apollo config found for project. For more information, please refer to: https://go.apollo.dev/t/config`
113
- );
114
- return null;
115
- }
116
-
117
74
  // add API key from the env
118
- let engineConfig = {},
119
- apiKey,
120
- nameFromKey;
75
+ let apiKey, nameFromKey;
121
76
 
122
77
  // loop over the list of possible .env files and try to parse for key
123
78
  // and service name. Files are scanned and found values are preferred
124
79
  // in order of appearance in `envFileNames`.
125
80
  envFileNames.forEach((envFile) => {
126
- const dotEnvPath = configPath
127
- ? resolve(configPath, envFile)
128
- : resolve(process.cwd(), envFile);
81
+ const dotEnvPath = resolve(configPath, envFile);
129
82
 
130
83
  if (existsSync(dotEnvPath) && lstatSync(dotEnvPath).isFile()) {
131
84
  const env: { [key: string]: string } = require("dotenv").parse(
132
- readFileSync(dotEnvPath)
85
+ readFileSync(dotEnvPath),
133
86
  );
134
- const legacyKey = env[legacyKeyEnvVar];
135
- const key = env[keyEnvVar];
136
- if (legacyKey && key) {
137
- Debug.warning(
138
- `Both ${legacyKeyEnvVar} and ${keyEnvVar} were found. ${keyEnvVar} will take precedence.`
139
- );
140
- }
141
- if (legacyKey) {
142
- Debug.warning(
143
- `[Deprecation warning] Setting the key via ${legacyKeyEnvVar} is deprecated and will not be supported in future versions. Please use ${keyEnvVar} instead.`
144
- );
145
- }
146
- apiKey = key || legacyKey;
87
+ apiKey = env[keyEnvVar];
147
88
  }
148
89
  });
149
90
 
150
91
  if (apiKey) {
151
- engineConfig = { engine: { apiKey } };
152
92
  nameFromKey = getServiceFromKey(apiKey);
153
93
  }
154
94
 
155
- // DETERMINE PROJECT TYPE
156
- // The CLI passes in a type when loading config. The editor extension
157
- // does not. So we determine the type of the config here, and use it if
158
- // the type wasn't explicitly passed in.
159
- let projectType: "client" | "service";
160
- if (type) {
161
- projectType = type;
162
- } else if (loadedConfig && loadedConfig.config.client) {
163
- projectType = "client";
164
- } else if (loadedConfig && loadedConfig.config.service) {
165
- projectType = "service";
166
- } else {
167
- Debug.error(
168
- "Unable to resolve project type. Please add either a client or service config. For more information, please refer to https://go.apollo.dev/t/config"
169
- );
170
- return null;
171
- }
172
-
173
- // DETERMINE SERVICE NAME
174
- // precedence: 1. (highest) config.js (client only) 2. name passed into loadConfig 3. name from api key
175
- let serviceName = name || nameFromKey;
176
- if (
177
- projectType === "client" &&
178
- loadedConfig &&
179
- loadedConfig.config.client &&
180
- typeof loadedConfig.config.client.service === "string"
181
- ) {
182
- serviceName = loadedConfig.config.client.service;
183
- }
184
-
185
- // if there wasn't a config loaded from a file, build one.
186
- // if there was a service name found in the env, merge it with the new/existing config object.
187
- // if the config loaded doesn't have a client/service key, add one based on projectType
188
- if (
189
- !loadedConfig ||
190
- serviceName ||
191
- !(loadedConfig.config.client || loadedConfig.config.service)
192
- ) {
193
- loadedConfig = {
194
- filepath: configPath || process.cwd(),
195
- config: {
196
- ...(loadedConfig && loadedConfig.config),
197
- ...(projectType === "client"
198
- ? {
199
- client: {
200
- ...DefaultConfigBase,
201
- ...(loadedConfig && loadedConfig.config.client),
202
- service: serviceName,
203
- },
204
- }
205
- : {
206
- service: {
207
- ...DefaultConfigBase,
208
- ...(loadedConfig && loadedConfig.config.service),
209
- name: serviceName,
210
- },
211
- }),
212
- },
213
- };
214
- }
215
-
216
95
  let { config, filepath } = loadedConfig;
217
96
 
218
- // selectively apply defaults when loading the config
219
- // this is just the includes/excludes defaults.
220
- // These need to go on _all_ configs. That's why this is last.
221
- if (config.client) config = merge({ client: DefaultClientConfig }, config);
222
- if (config.service) config = merge({ service: DefaultServiceConfig }, config);
223
- if (engineConfig) config = merge(engineConfig, config);
224
-
225
- config = merge({ engine: DefaultEngineConfig }, config);
226
-
227
- return new ApolloConfig(config, URI.file(resolve(filepath)));
97
+ return parseApolloConfig(config, URI.file(resolve(filepath)), {
98
+ apiKey,
99
+ serviceName: nameFromKey,
100
+ });
228
101
  }