Skip to content

Commit

Permalink
Refactored loadConfig and documented args
Browse files Browse the repository at this point in the history
  • Loading branch information
JakeDawkins committed Mar 26, 2019
1 parent 3030711 commit a41f2da
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 92 deletions.
119 changes: 70 additions & 49 deletions packages/apollo-language-server/src/config/__tests__/loadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,22 +55,23 @@ describe("loadConfig", () => {
dir = dirPath = undefined;
});

it("loads with client defaults from different dir", async () => {
writeFilesToDir(dir, {
"my.config.js": `
module.exports = {
client: {
service: 'hello'
describe("finding files", () => {
it("loads with client defaults from different dir", async () => {
writeFilesToDir(dir, {
"my.config.js": `
module.exports = {
client: {
service: 'hello'
}
}
}
`
});
`
});

const config = await loadConfig({
configPath: dirPath,
configFileName: "my.config.js"
});
expect(config.rawConfig).toMatchInlineSnapshot(`
const config = await loadConfig({
configPath: dirPath,
configFileName: "my.config.js"
});
expect(config.rawConfig).toMatchInlineSnapshot(`
Object {
"client": Object {
"addTypename": true,
Expand Down Expand Up @@ -102,24 +103,24 @@ Object {
},
}
`);
});
});

it("loads with service defaults from different dir", async () => {
writeFilesToDir(dir, {
"my.config.js": `
module.exports = {
service: {
name: 'hello'
it("loads with service defaults from different dir", async () => {
writeFilesToDir(dir, {
"my.config.js": `
module.exports = {
service: {
name: 'hello'
}
}
}
`
});
`
});

const config = await loadConfig({
configPath: dirPath,
configFileName: "my.config.js"
});
expect(config.rawConfig).toMatchInlineSnapshot(`
const config = await loadConfig({
configPath: dirPath,
configFileName: "my.config.js"
});
expect(config.rawConfig).toMatchInlineSnapshot(`
Object {
"engine": Object {
"endpoint": "https://engine-graphql.apollographql.com/api/graphql",
Expand All @@ -140,24 +141,25 @@ Object {
},
}
`);
});

it("[deprecated] loads config from package.json", async () => {
writeFilesToDir(dir, {
"package.json": `{"apollo":{"client": {"service": "hello"}} }`
});
const config = await loadConfig({ configPath: dirPath });

expect(config.client.service).toEqual("hello");
});
it("[deprecated] loads config from package.json", async () => {
writeFilesToDir(dir, {
"package.json": `{"apollo":{"client": {"service": "hello"}} }`
});
const config = await loadConfig({ configPath: dirPath });

it("loads config from a ts file", async () => {
writeFilesToDir(dir, {
"apollo.config.ts": `module.exports = {"client": {"service": "hello"}`
expect(config.client.service).toEqual("hello");
});
const config = await loadConfig({ configPath: dirPath });

expect(config.client.service).toEqual("hello");
it("loads config from a ts file", async () => {
writeFilesToDir(dir, {
"apollo.config.ts": `module.exports = {"client": {"service": "hello"}`
});
const config = await loadConfig({ configPath: dirPath });

expect(config.client.service).toEqual("hello");
});
});

describe("errors", () => {
Expand All @@ -166,8 +168,7 @@ Object {

return loadConfig({
configPath: dirPath,
configFileName: "my.config.js",
loadExactOnly: true
configFileName: "my.config.js"
}).catch(err => {
expect(err.message).toMatch(/.*A config file failed to load at.*/);
done();
Expand All @@ -179,8 +180,7 @@ Object {

return loadConfig({
configPath: dirPath,
configFileName: "my.config.js",
loadExactOnly: true
configFileName: "my.config.js"
}).catch(err => {
expect(err.message).toMatch(
/.*A config file failed to load with options.*/
Expand All @@ -198,8 +198,7 @@ Object {

await loadConfig({
configPath: dirPath,
configFileName: "package.json",
loadExactOnly: true
configFileName: "package.json"
});

expect(console.warn.mock.calls[0][0]).toMatchInlineSnapshot(
Expand All @@ -212,7 +211,6 @@ Object {

return loadConfig({
configFileName: "my.TYPO.js",
loadExactOnly: true,
requireConfig: true // this is what we're testing
}).catch(err => {
expect(err.message).toMatch(/.*No Apollo config found for project*/);
Expand All @@ -236,4 +234,27 @@ Object {
);
});
});

describe("env loading", () => {
it("finds .env in config path", () => {});
it("finds .env in cwd", () => {});
it("parses .env for api key and service name", () => {});
});
describe("project type", () => {
it("uses passed in type as override", () => {});
it("infers client projects", () => {});
it("infers service projects", () => {});
it("throws if project type cant be inferred", () => {});
});
describe("service name", () => {
it("lets config service name take precedence for client project", () => {});
it("lets name passed in take precedence over env var", () => {});
it("uses env var to determine service name when no other options", () => {});
});
describe("default merging", () => {
it("merges service name and default config for client projects", () => {});
it("merges service name and default config for service projects", () => {});
it("merges engine config with projects", () => {});
it("merges defaults in at the end", () => {});
});
});
88 changes: 45 additions & 43 deletions packages/apollo-language-server/src/config/loadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { LoaderEntry } from "cosmiconfig";
import TypeScriptLoader from "@endemolshinegroup/cosmiconfig-typescript-loader";
import { resolve } from "path";
import { readFileSync, existsSync } from "fs";
import { merge } from "lodash/fp";
import { merge, get } from "lodash/fp";
import {
ApolloConfig,
ApolloConfigFormat,
Expand All @@ -17,18 +17,12 @@ import URI from "vscode-uri";

// config settings
const MODULE_NAME = "apollo";
const defaultSearchPlaces = [
const defaultFileNames = [
"package.json",
`${MODULE_NAME}.config.js`,
`${MODULE_NAME}.config.ts`
];

// Based on order, a provided config file will take precedence over the defaults
const getSearchPlaces = (configFile?: string, loadExactOnly?: boolean) =>
loadExactOnly && configFile
? [configFile]
: [...(configFile ? [configFile] : []), ...defaultSearchPlaces];

const loaders = {
// XXX improve types for config
".json": (cosmiconfig as any).loadJson as LoaderEntry,
Expand All @@ -42,12 +36,23 @@ export interface LoadConfigSettings {
// the current working directory to start looking for the config
// config loading only works on node so we default to
// process.cwd()

// configPath and fileName are used in conjunction with one another.
// i.e. /User/myProj/my.config.js
// => { configPath: '/User/myProj/', configFileName: 'my.config.js' }
configPath?: string;

// if a configFileName is passed in, loadConfig won't accept any other
// configs as a fallback.
configFileName?: string;

// used when run by a `Workspace` where we _know_ a config file should be present.
requireConfig?: boolean;

// for CLI usage, we don't _require_ a config file for everything. This allows us to pass in
// options to build one at runtime
name?: string;
type?: "service" | "client";
loadExactOnly?: boolean; // match the configFileName EXACTLY. Don't allow defaults
}

export type ConfigResult<T> = {
Expand All @@ -61,15 +66,14 @@ export async function loadConfig({
configFileName,
requireConfig = false,
name,
type,
loadExactOnly
type
}: LoadConfigSettings) {
const explorer = cosmiconfig(MODULE_NAME, {
searchPlaces: getSearchPlaces(configFileName, loadExactOnly),
searchPlaces: configFileName ? [configFileName] : defaultFileNames,
loaders
});

// search can fail if a file can't be parsed (ex: a nonsense js file)
// search can fail if a file can't be parsed (ex: a nonsense js file) so we wrap in a try/catch
let loadedConfig;
try {
loadedConfig = (await explorer.search(configPath)) as ConfigResult<
Expand Down Expand Up @@ -103,9 +107,11 @@ export async function loadConfig({
);
}

// add API to the env
// add API key from the env
let engineConfig = {},
nameFromKey;

// if there's a .env file, load it and parse for key and service name
const dotEnvPath = configPath
? resolve(configPath, ".env")
: resolve(process.cwd(), ".env");
Expand All @@ -114,63 +120,57 @@ export async function loadConfig({
const env: { [key: string]: string } = require("dotenv").parse(
readFileSync(dotEnvPath)
);

if (env["ENGINE_API_KEY"]) {
engineConfig = { engine: { apiKey: env["ENGINE_API_KEY"] } };
nameFromKey = getServiceFromKey(env["ENGINE_API_KEY"]);
}
}

let resolvedName = name || nameFromKey;

// DETERMINE PROJECT TYPE
// The CLI passes in a type when loading config. The editor extension
// does not. So we determine the type of the config here, and use it if
// the type wasn't explicitly passed in.
let resolvedType: "client" | "service";
if (type) {
resolvedType = type;
if (
loadedConfig &&
loadedConfig.config.client &&
typeof loadedConfig.config.client.service === "string"
) {
resolvedName = loadedConfig.config.client.service;
}
} else if (loadedConfig && loadedConfig.config.client) {
resolvedType = "client";
resolvedName =
typeof loadedConfig.config.client.service === "string"
? loadedConfig.config.client.service
: resolvedName;
} else if (loadedConfig && loadedConfig.config.service) {
resolvedType = "service";
} else {
let projectType: "client" | "service";
if (type) projectType = type;
else if (loadedConfig && loadedConfig.config.client) projectType = "client";
else if (loadedConfig && loadedConfig.config.service) projectType = "service";
else
throw new Error(
"Unable to resolve project type. Please add either a client or service config. For more information, please refer to https://bit.ly/2ByILPj"
);

// DETERMINE SERVICE NAME
// precedence: 1. (highest) config.js (client only) 2. name passed into loadConfig 3. name from api key
let serviceName = name || nameFromKey;
if (
projectType === "client" &&
loadedConfig &&
loadedConfig.config.client &&
typeof loadedConfig.config.client.service === "string"
) {
serviceName = loadedConfig.config.client.service;
}

// If there's a name passed in (from env/flag), it merges with the config file, to
// overwrite either the client's service (if a client project), or the service's name.
// if there's no config file, it uses the `DefaultConfigBase` to fill these in.
if (!loadedConfig || resolvedName) {
// if there wasn't a config loaded from a file, build one.
// if there was a service name found in the env, merge it with the new/existing config object.
if (!loadedConfig || serviceName) {
loadedConfig = {
filepath: configPath || process.cwd(),
config: {
...(loadedConfig && loadedConfig.config),
...(resolvedType === "client"
...(projectType === "client"
? {
client: {
...DefaultConfigBase,
...(loadedConfig && loadedConfig.config.client),
service: resolvedName
service: serviceName
}
}
: {
service: {
...DefaultConfigBase,
...(loadedConfig && loadedConfig.config.service),
name: resolvedName
name: serviceName
}
})
}
Expand All @@ -180,6 +180,8 @@ export async function loadConfig({
let { config, filepath } = loadedConfig;

// selectivly apply defaults when loading the config
// this is just the includes/excludes defaults.
// These need to go on _all_ configs. That's why this is last.
if (config.client) config = merge({ client: DefaultClientConfig }, config);
if (config.service) config = merge({ service: DefaultServiceConfig }, config);
if (engineConfig) config = merge(engineConfig, config);
Expand Down

0 comments on commit a41f2da

Please sign in to comment.