feat(core): auto detect proxy server on load (#3195)

* feat: auto detect proxy server on load

* fix: opt-in for auto proxy server detection
This commit is contained in:
Erez Rokah 2020-02-05 17:56:11 +02:00 committed by GitHub
parent 2043c0b782
commit 614f1aea63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 158 additions and 13 deletions

View File

@ -1,11 +1,15 @@
backend:
name: proxy
name: github
branch: master
proxy_url: http://localhost:8082/api/v1
repo: owner/repo
publish_mode: editorial_workflow
media_folder: static/media
public_folder: /media
local_backend:
url: http://localhost:8082/api/v1
collections:
- name: posts
label: Posts

View File

@ -1,5 +1,7 @@
import { fromJS } from 'immutable';
import { applyDefaults } from '../config';
import { applyDefaults, detectProxyServer } from '../config';
jest.spyOn(console, 'log').mockImplementation(() => {});
describe('config', () => {
describe('applyDefaults', () => {
@ -168,4 +170,66 @@ describe('config', () => {
});
});
});
describe('detectProxyServer', () => {
const assetFetchCalled = (url = 'http://localhost:8081/api/v1') => {
expect(global.fetch).toHaveBeenCalledTimes(1);
expect(global.fetch).toHaveBeenCalledWith(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'info' }),
});
};
beforeEach(() => {
delete window.location;
});
it('should return undefined when not on localhost', async () => {
window.location = { hostname: 'www.netlify.com' };
global.fetch = jest.fn();
await expect(detectProxyServer()).resolves.toBeUndefined();
expect(global.fetch).toHaveBeenCalledTimes(0);
});
it('should return undefined when fetch returns an error', async () => {
window.location = { hostname: 'localhost' };
global.fetch = jest.fn().mockRejectedValue(new Error());
await expect(detectProxyServer(true)).resolves.toBeUndefined();
assetFetchCalled();
});
it('should return undefined when fetch returns an invalid response', async () => {
window.location = { hostname: 'localhost' };
global.fetch = jest
.fn()
.mockResolvedValue({ json: jest.fn().mockResolvedValue({ repo: [] }) });
await expect(detectProxyServer(true)).resolves.toBeUndefined();
assetFetchCalled();
});
it('should return proxyUrl when fetch returns a valid response', async () => {
window.location = { hostname: 'localhost' };
global.fetch = jest
.fn()
.mockResolvedValue({ json: jest.fn().mockResolvedValue({ repo: 'test-repo' }) });
await expect(detectProxyServer(true)).resolves.toBe('http://localhost:8081/api/v1');
assetFetchCalled();
});
it('should use local_backend url', async () => {
const url = 'http://localhost:8082/api/v1';
window.location = { hostname: 'localhost' };
global.fetch = jest
.fn()
.mockResolvedValue({ json: jest.fn().mockResolvedValue({ repo: 'test-repo' }) });
await expect(detectProxyServer({ url })).resolves.toBe(url);
assetFetchCalled(url);
});
});
});

View File

@ -1,6 +1,6 @@
import yaml from 'js-yaml';
import { Map, fromJS } from 'immutable';
import { trimStart, get } from 'lodash';
import { trimStart, get, isPlainObject } from 'lodash';
import { authenticateUser } from 'Actions/auth';
import * as publishModes from 'Constants/publishModes';
import { validateConfig } from 'Constants/configSchema';
@ -145,6 +145,31 @@ export function mergeConfig(config) {
return { type: CONFIG_MERGE, payload: config };
}
export async function detectProxyServer(localBackend) {
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
let proxyUrl;
if (localBackend === true) {
proxyUrl = 'http://localhost:8081/api/v1';
} else if (isPlainObject(localBackend)) {
proxyUrl = localBackend.url;
}
try {
console.log(`Looking for Netlify CMS Proxy Server at '${proxyUrl}'`);
const { repo } = await fetch(`${proxyUrl}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'info' }),
}).then(res => res.json());
if (typeof repo === 'string') {
console.log(`Detected Netlify CMS Proxy Server at '${proxyUrl}' with repo: '${repo}'`);
return proxyUrl;
}
} catch {
console.log(`Netlify CMS Proxy Server not detected at '${proxyUrl}'`);
}
}
}
export function loadConfig() {
if (window.CMS_CONFIG) {
return configDidLoad(fromJS(window.CMS_CONFIG));
@ -155,17 +180,29 @@ export function loadConfig() {
try {
const preloadedConfig = getState().config;
const configUrl = getConfigUrl();
const isPreloaded = preloadedConfig && preloadedConfig.size > 1;
const loadedConfig =
preloadedConfig && preloadedConfig.get('load_config_file') === false
? {}
: await getConfig(configUrl, preloadedConfig && preloadedConfig.size > 1);
: await getConfig(configUrl, isPreloaded);
/**
* Merge any existing configuration so the result can be validated.
*/
const mergedConfig = mergePreloadedConfig(preloadedConfig, loadedConfig);
let mergedConfig = mergePreloadedConfig(preloadedConfig, loadedConfig);
validateConfig(mergedConfig.toJS());
// detect running Netlify CMS proxy
if (mergedConfig.has('local_backend')) {
const proxyUrl = await detectProxyServer(mergedConfig.toJS().local_backend);
if (proxyUrl) {
mergedConfig = mergePreloadedConfig(mergedConfig, {
backend: { name: 'proxy', proxy_url: proxyUrl },
});
}
}
const config = applyDefaults(mergedConfig);
dispatch(configDidLoad(config));

View File

@ -126,5 +126,31 @@ describe('config', () => {
});
}).toThrowError("'collections[0]' should be object");
});
it('should throw if local_backend is not a boolean or plain object', () => {
expect(() => {
validateConfig(merge(validConfig, { local_backend: [] }));
}).toThrowError("'local_backend' should be boolean");
});
it('should throw if local_backend is a plain object but missing url property', () => {
expect(() => {
validateConfig(merge(validConfig, { local_backend: {} }));
}).toThrowError("'local_backend' should be object");
});
it('should not throw if local_backend is a boolean', () => {
expect(() => {
validateConfig(merge(validConfig, { local_backend: true }));
}).not.toThrowError();
});
it('should not throw if local_backend is a plain object with url property', () => {
expect(() => {
validateConfig(
merge(validConfig, { local_backend: { url: 'http://localhost:8081/api/v1' } }),
);
}).not.toThrowError();
});
});
});

View File

@ -42,6 +42,18 @@ const getConfigSchema = () => ({
},
required: ['name'],
},
local_backend: {
oneOf: [
{ type: 'boolean' },
{
type: 'object',
properties: {
url: { type: 'string', examples: ['http://localhost:8081/api/v1'] },
},
required: ['url'],
},
],
},
locale: { type: 'string', examples: ['en', 'fr', 'de'] },
site_url: { type: 'string', examples: ['https://example.com'] },
display_url: { type: 'string', examples: ['https://example.com'] },

View File

@ -14,18 +14,20 @@ You can connect Netlify CMS to a local Git repository, instead of working with a
1. Navigate to a local Git repository configured with the CMS.
2. Run `npx netlify-cms-proxy-server` from the root directory of the above repository.
3. Update your `config.yml` to connect to the server:
3. Add the `local_backend` configuration to your `config.yml`:
```yaml
backend:
name: proxy
proxy_url: http://localhost:8081/api/v1
branch: master # optional, defaults to master
# when using the default proxy server port
local_backend: true
# when using a custom proxy server port
local_backend:
url: http://localhost:8082/api/v1
```
4. Start you local development server (e.g. run `gatsby develop`).
4. Start your local development server (e.g. run `gatsby develop`).
> `netlify-cms-proxy-server` runs an unauthenticated express server. As any client can send requests to the server, it should only be used for local development.
**Note:** `netlify-cms-proxy-server` runs an unauthenticated express server. As any client can send requests to the server, it should only be used for local development.
## GitLab and BitBucket Editorial Workflow Support