diff --git a/dev-test/backends/proxy/config.yml b/dev-test/backends/proxy/config.yml index cefad1a1..4c07e4dd 100644 --- a/dev-test/backends/proxy/config.yml +++ b/dev-test/backends/proxy/config.yml @@ -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 diff --git a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js index 922ee800..f2a6173a 100644 --- a/packages/netlify-cms-core/src/actions/__tests__/config.spec.js +++ b/packages/netlify-cms-core/src/actions/__tests__/config.spec.js @@ -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); + }); + }); }); diff --git a/packages/netlify-cms-core/src/actions/config.js b/packages/netlify-cms-core/src/actions/config.js index 973affb1..93fd878f 100644 --- a/packages/netlify-cms-core/src/actions/config.js +++ b/packages/netlify-cms-core/src/actions/config.js @@ -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)); diff --git a/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js b/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js index 6407f86a..46fa29dd 100644 --- a/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js +++ b/packages/netlify-cms-core/src/constants/__tests__/configSchema.spec.js @@ -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(); + }); }); }); diff --git a/packages/netlify-cms-core/src/constants/configSchema.js b/packages/netlify-cms-core/src/constants/configSchema.js index 1f2c6ac8..5bb2bbdc 100644 --- a/packages/netlify-cms-core/src/constants/configSchema.js +++ b/packages/netlify-cms-core/src/constants/configSchema.js @@ -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'] }, diff --git a/website/content/docs/beta-features.md b/website/content/docs/beta-features.md index f4a57e55..7ece9999 100644 --- a/website/content/docs/beta-features.md +++ b/website/content/docs/beta-features.md @@ -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