Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change lookup of '.runtimeconfig.json' as relative to cwd #542

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"jsdom": "^16.2.1",
"jsonwebtoken": "^8.5.1",
"mocha": "^6.1.4",
"mock-fs": "^4.13.0",
"mock-require": "^3.0.3",
"mz": "^2.7.0",
"nock": "^10.0.6",
Expand Down
47 changes: 46 additions & 1 deletion spec/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
// SOFTWARE.

import { expect } from 'chai';
import * as mockFs from 'mock-fs';
import * as mockRequire from 'mock-require';
import { config, firebaseConfig } from '../src/config';

Expand All @@ -32,19 +33,63 @@ describe('config()', () => {
delete process.env.PWD;
});
afterEach(() => {
mockFs.restore();
mockRequire.stopAll();
delete config.singleton;
delete process.env.FIREBASE_CONFIG;
delete process.env.CLOUD_RUNTIME_CONFIG;
});

it('loads config values from .runtimeconfig.json', () => {
mockRequire('/srv/.runtimeconfig.json', { foo: 'bar', firebase: {} });
const configPath = '/srv/.runtimeconfig.json';
const mockConfig = {
foo: 'bar',
firebase: {},
};
mockRequire(configPath, mockConfig);
mockFs({ [configPath]: JSON.stringify(mockConfig) });

const loaded = config();
expect(loaded).to.not.have.property('firebase');
expect(loaded).to.have.property('foo', 'bar');
});

it('loads config values from hoisted .runtimeconfig.json', () => {
const hoistedConfigPath = '/.runtimeconfig.json'; // One directory up from /srv
const mockConfig = {
foo: 'bar',
firebase: {},
};
mockRequire(hoistedConfigPath, mockConfig);
mockFs({ [hoistedConfigPath]: JSON.stringify(mockConfig) });

const loaded = config();
expect(loaded).to.not.have.property('firebase');
expect(loaded).to.have.property('foo', 'bar');
});

it('loads config values from node_modules sibling before other hoisted .runtimeconfig.json files', () => {
const hoistedConfigPath = '/.runtimeconfig.json'; // One directory up from /srv
const nodeModulesSiblingConfigPath = '/srv/.runtimeconfig.json';
const configValid = {
foo: 'bar',
firebase: {},
};
const configInvalid = {
foo: 'baz',
firebase: {},
};
mockRequire(hoistedConfigPath, configInvalid);
mockRequire(nodeModulesSiblingConfigPath, configValid);
mockFs({
[hoistedConfigPath]: JSON.stringify(configInvalid),
[nodeModulesSiblingConfigPath]: JSON.stringify(configValid),
});

const loaded = config();
expect(loaded).to.have.property('foo', configValid.foo);
});

it('does not provide firebase config if .runtimeconfig.json not invalid', () => {
mockRequire('/srv/.runtimeconfig.json', 'does-not-exist');
expect(firebaseConfig()).to.be.null;
Expand Down
30 changes: 22 additions & 8 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import * as firebase from 'firebase-admin';
import * as path from 'path';
import { existsSync, findUpSync } from './utilities/fs';

export function config(): config.Config {
if (typeof config.singleton === 'undefined') {
Expand Down Expand Up @@ -50,6 +51,25 @@ export namespace config {
export let singleton: config.Config;
}

/** @hidden */
function findConfigPath(): string {
// Config path preferences in order:
// 1. ENV value CLOUD_RUNTIME_CONFIG.
// 2. The .runtimeconfig.json in the working directory.
// 3. The .runtimeconfig.json in ancestor directory (traversing up from PWD).

if (process.env.CLOUD_RUNTIME_CONFIG) {
return process.env.CLOUD_RUNTIME_CONFIG as string;
}

const configPathAtPwd = path.join(process.env.PWD, '.runtimeconfig.json');
if (existsSync(configPathAtPwd)) {
return configPathAtPwd;
}

return findUpSync('.runtimeconfig.json') as string;
}

/** @hidden */
export function firebaseConfig(): firebase.AppOptions | null {
const env = process.env.FIREBASE_CONFIG;
Expand All @@ -69,10 +89,7 @@ export function firebaseConfig(): firebase.AppOptions | null {

// Could have Runtime Config with Firebase in it as an ENV location or default.
try {
const configPath =
process.env.CLOUD_RUNTIME_CONFIG ||
path.join(process.env.PWD, '.runtimeconfig.json');
const config = require(configPath);
const config = require(findConfigPath());
if (config.firebase) {
return config.firebase;
}
Expand All @@ -94,10 +111,7 @@ function init() {
}

try {
const configPath =
process.env.CLOUD_RUNTIME_CONFIG ||
path.join(process.env.PWD, '.runtimeconfig.json');
const parsed = require(configPath);
const parsed = require(findConfigPath());
delete parsed.firebase;
config.singleton = parsed;
return;
Expand Down
97 changes: 97 additions & 0 deletions src/utilities/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import * as fs from 'fs';
import * as path from 'path';

/**
* Like fs.existsSync, but fails softly.
* @param path A path to check exists.
*/
export function existsSync(path): boolean | void {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's rename this to something like safeExistsSync to indicate the behavioral difference from standard existsSync.

try {
return fs.existsSync(path);
} catch (err) {
return undefined;
}
}

type PathType = 'file' | 'directory';

interface LocatePathOptions {
cwd?: string;
allowSymlinks?: boolean;
type?: PathType;
}

const stop = Symbol('findUp.stop');

const pathTypeMap: { [K in PathType]: string } = {
directory: 'isDirectory',
file: 'isFile',
};

function locatePathSync(paths, options: LocatePathOptions) {
options = {
cwd: process.cwd(),
allowSymlinks: true,
type: 'file',
...options,
};

if (!(options.type in pathTypeMap)) {
throw new Error(`Invalid type specified: ${options.type}`);
}

const statFn = options.allowSymlinks ? fs.statSync : fs.lstatSync;

for (const currentPath of paths) {
try {
const stat = statFn(path.resolve(options.cwd, currentPath));

if (options.type === undefined || stat[pathTypeMap[options.type]]()) {
return currentPath;
}
} catch {}
}
}

/**
* Finds the closest file matching the given file name, traversing up.
* @param fileName The filename to look up, starting from cwd.
* @param options The options change findUp behaviour.
*/
export function findUpSync(
fileName,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: filename is the initialism I'm familiar with.

options: LocatePathOptions = {}
): string | void {
let directory = path.resolve(options.cwd || '');
const { root } = path.parse(directory);
const paths = [].concat(fileName);

const runMatcher = (locateOptions) => {
if (typeof fileName !== 'function') {
return locatePathSync(paths, locateOptions);
}

const foundPath = fileName(locateOptions.cwd);
if (typeof foundPath === 'string') {
return locatePathSync([foundPath], locateOptions);
}

return foundPath;
};

while (true) {
const foundPath = runMatcher({ ...options, cwd: directory });

if (foundPath === stop) {
return;
}
if (foundPath) {
return path.resolve(directory, foundPath);
}
if (directory === root) {
return;
}

directory = path.dirname(directory);
}
}