├── .gitignore ├── .travis.yml ├── ABOUT.md ├── LICENSE ├── README.md ├── babel.config.js ├── lerna.json ├── package.json ├── packages ├── netlify-cms-backend-fs │ ├── .eslintrc │ ├── .gitignore │ ├── .prettierrc │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── scripts │ │ ├── _tests_ │ │ │ └── simple.js │ │ └── fs │ │ │ ├── fs-api.js │ │ │ ├── fs-express-api.js │ │ │ └── index.js │ ├── src │ │ ├── API.js │ │ ├── AuthenticationPage.js │ │ ├── components │ │ │ ├── Authentication.js │ │ │ └── FolderIcon.js │ │ ├── implementation.js │ │ └── lib │ │ │ ├── APIError.js │ │ │ └── pathHelper.js │ └── webpack.config.js └── netlify-cms-starter │ ├── .gitignore │ ├── README.md │ ├── example │ ├── recipes.json │ └── test.yml │ ├── package.json │ ├── public │ ├── assets │ │ └── media │ │ │ └── logo.svg │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── uploads │ │ └── .gitkeep │ └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── components │ └── NetlifyCMS │ │ ├── components │ │ ├── AuthorsPreview │ │ │ └── index.js │ │ └── EditorYoutube │ │ │ └── index.js │ │ ├── data │ │ └── config.json │ │ └── index.js │ ├── index.css │ ├── index.js │ ├── serviceWorker.js │ └── setupProxy.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/*.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | cache: 5 | yarn: true 6 | directories: 7 | - $HOME/.npm 8 | - $HOME/.yarn-cache 9 | - node_modules 10 | 11 | branches: 12 | only: 13 | - master 14 | 15 | install: 16 | - yarn 17 | - lerna bootstrap 18 | 19 | script: 20 | - yarn workspace netlify-cms-backend-fs build:copy 21 | - yarn workspace netlify-cms-backend-fs build:lib 22 | 23 | after_success: 24 | - yarn workspace netlify-cms-backend-fs semantic-release 25 | -------------------------------------------------------------------------------- /ABOUT.md: -------------------------------------------------------------------------------- 1 | Mono-Repo for NetlifyCMS Components 2 | 3 | Uses [Lerna][Lerna] and [Yarn Workspaces][Yarn Workspaces] 4 | 5 | `yarn start` 6 | 7 | [Lerna]: https://lernajs.io/ 8 | [Yarn Workspaces]: https://yarnpkg.com/lang/en/docs/workspaces/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ADARTA Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Open source components for NetlifyCMS 2 | 3 | ## Packages (`packages/`) 4 | 5 | ### [netlify-cms-backend-fs][1] (deprecated for [official solution](https://www.netlifycms.org/docs/beta-features/#working-with-a-local-git-repository)) 6 | 7 | Go to [https://github.com/ADARTA/netlify-cms-react-example](https://github.com/ADARTA/netlify-cms-react-example) to see an example of the new proxy server being used. 8 | 9 | ### [netlify-cms-starter][2] 10 | 11 | Example of a create-react-app using NetlifyCMS as a component. 12 | 13 | Uses [netlify-cms-backend-fs][1] to show how to include in cra. 14 | 15 | See [netlify-cms-cra][3] also. 16 | 17 | [1]: https://github.com/ADARTA/netlify-cms-components/tree/master/packages/netlify-cms-backend-fs 18 | [2]: https://github.com/ADARTA/netlify-cms-components/tree/master/packages/netlify-cms-starter 19 | [3]: https://github.com/adarta/netlify-cms-react-example 20 | [4]: https://github.com/adarta/netlify-cms-components 21 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | console.log("Here be babel!") 2 | 3 | module.exports = { 4 | "presets": [ 5 | // modules false, because Babel will convert our modules to CommonJS before Webpack 6 | ["@babel/env", {"modules": false}], 7 | "@babel/preset-react" 8 | ], 9 | "plugins": [] 10 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "3.8.0", 3 | "packages": ["packages/*"], 4 | "version": "independent", 5 | "npmClient": "yarn", 6 | "useWorkspaces": true 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "packages/*" 5 | ], 6 | "scripts": { 7 | "bootstrap": "yarn && lerna bootstrap", 8 | "changelog": "echo \"npx conventional-changelog-cli -p angular -i CHANGELOG.md -s -r 0\"", 9 | "commit": "git-cz", 10 | "start": "yarn workspace netlify-cms-starter start", 11 | "serve:build": "serve -s packages/netlify-cms-starter/build", 12 | "build": "lerna run build", 13 | "build:cms": "yarn workspace @talves/netlify-cms-manual build", 14 | "build:fs": "yarn workspace netlify-cms-backend-fs build", 15 | "build:starter": "cd packages/netlify-cms-starter && yarn build" 16 | }, 17 | "devDependencies": { 18 | "@babel/core": "^7.2.2", 19 | "@babel/preset-env": "^7.2.3", 20 | "@babel/preset-react": "^7.0.0", 21 | "babel-loader": "8.0.5", 22 | "commitizen": "3.0.5", 23 | "cp-cli": "2.0.0", 24 | "cross-env": "5.2.0", 25 | "cz-conventional-changelog": "2.1.0", 26 | "eslint": "5.12.0", 27 | "eslint-plugin-react-hooks": "1.0.1", 28 | "lerna": "^3.8.0", 29 | "npm-run-all": "4.1.5", 30 | "prettier": "^1.15.3", 31 | "semantic-release": "^15.13.3", 32 | "semantic-release-cli": "4.1.0", 33 | "style-loader": "^0.23.1", 34 | "svg-inline-loader": "^0.8.0", 35 | "webpack": "4.28.3", 36 | "webpack-cli": "3.2.3" 37 | }, 38 | "config": { 39 | "commitizen": { 40 | "path": "node_modules/cz-conventional-changelog" 41 | } 42 | }, 43 | "browserslist": [ 44 | "last 2 Chrome versions", 45 | "last 2 Opera versions", 46 | "last 2 Firefox versions", 47 | "last 2 Edge versions", 48 | "last 2 Safari versions", 49 | "last 2 iOS versions", 50 | "last 2 ChromeAndroid versions" 51 | ], 52 | "dependencies": {} 53 | } 54 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:react/recommended" 6 | ], 7 | "env": { 8 | "es6": true, 9 | "browser": true, 10 | "node": true 11 | }, 12 | "globals": { 13 | "FILESYSTEMBACKEND_VERSION": false 14 | }, 15 | "plugins": [ 16 | "react-hooks" 17 | ], 18 | "rules": { 19 | "no-console": [0], 20 | "react-hooks/rules-of-hooks": "error" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | 4 | .vscode/ 5 | 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 ADARTA Inc. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/README.md: -------------------------------------------------------------------------------- 1 | # [[Deprecated for Official Solution](https://www.netlifycms.org/docs/beta-features/#working-with-a-local-git-repository)] 2 | 3 | ## Custom Backend for NetlifyCMS 4 | 5 | [![Build Status](https://img.shields.io/travis/ADARTA/netlify-cms-components/master.svg?style=flat-square)](https://travis-ci.org/ADARTA/netlify-cms-components) 6 | [![](https://img.shields.io/npm/v/netlify-cms-backend-fs.svg?style=flat-square)](https://www.npmjs.com/package/netlify-cms-backend-fs) 7 | 8 | ***Notes:*** 9 | 10 | - This library is still in beta! 11 | - Version 0.4.0 is a breaking change 🐉. 12 | - Version 0.4.0+ is only compatible with builds of `netlify-cms-app` (2.9.0+). 13 | - This is a backend library for NetlifyCMS proposed for file system testing locally during development. 14 | - Handy for testing your config files. 15 | 16 | To use: 17 | 18 | To load dependencies for build 19 | 20 | ```bash 21 | yarn add netlify-cms-backend-fs --dev 22 | ``` 23 | 24 | or 25 | 26 | ```bash 27 | npm install netlify-cms-backend-fs --save-dev 28 | ``` 29 | ## Parts of this package 30 | 31 | Backend library bundles exist in `dist` directory. 32 | 33 | - `dist/index.js` can be used for global access to `FileSystemBackendClass` and is a `umd` build to use directly as a component see example in `netlify-cms-starter` in this monorepo. 34 | 35 | Express server middleware is in the `dist/fs` directory. 36 | 37 | - `dist/fs/index.js` (not bundled) has the node script to be used as middleware for webpack devServer or express server to create the api for development. 38 | 39 | ## How to register with CMS on a static page locally (testing only, not recommended) 40 | 41 | - Change the `index.html` page to use the backend as in the example below 42 | - Register the backend Class to the CMS as shown below 43 | - Change the `config.yml` backend to `backend: file-system` or the name you registered 44 | - [Webpack] Add devServer middleware to expose the `/api` path for the file-system API 45 | - [Stand Alone Server] Create an express server (example coming soon) to host the `/api` endpoint 46 | 47 | ### Add script and register in your CMS page 48 | 49 | **_NOTE:_** v4.x of this library will not work without a current version of `netlify-cms-app` (see notes at the top of this document). 50 | 51 | ```html 52 | 53 | 54 | 55 | 56 | 57 | NetlifyCMS 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 71 | 72 | 73 | ``` 74 | ### Start your devServer using the middleware scripts 75 | 76 | `server.js` 77 | ```javascript 78 | const express = require('express') 79 | const fsMiddleware = require('netlify-cms-backend-fs/dist/fs') 80 | const app = express() 81 | const port = 3000 82 | const host = 'localhost' 83 | 84 | app.use(express.static('.')) // root of our site 85 | 86 | // add cors code here (shown below) if you have a cors issue 87 | 88 | fsMiddleware(app) // sets up the /api proxy paths 89 | 90 | app.listen(port, () => console.log( 91 | ` 92 | Server listening at http://${host}:${port}/ 93 | API listening at http://${host}:${port}/api 94 | ` 95 | )) 96 | ``` 97 | ### Cors issue 98 | 99 | If having a cors problem when running on different ports, you can add the following to the express app. 100 | 101 | ``` 102 | var allowCrossDomain = function(req, res, next) { 103 | res.header('Access-Control-Allow-Origin', '*'); 104 | res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); 105 | res.header('Access-Control-Allow-Headers', 'Content-Type'); 106 | next(); 107 | } 108 | app.use(allowCrossDomain); 109 | ``` 110 | 111 | ## Some examples of `netlify-cms-backend-fs` in projects 112 | 113 | - see the [netlify-cms-starter][1] for a create-react-app example in this monorepo. 114 | - see [ADARTA/netlify-cms-react-example][4] for a full create-react-app example. 115 | - see [ADARTA/gatsby-starter-netlify-cms][5] for a Gatsby use case example (WIP). 116 | 117 | 118 | ## Dependencies 119 | 120 | This library requires you to be using [NetlifyCMS][3] v2.9.x or above (see notes at the top). 121 | 122 | ***Recommendation:*** If you are looking to extend NetlifyCMS and run a local file-system setup for development, use the [netlify-cms-react-example][4] starter project. It implements the backend as a component and bundles to a custom CMS deployment for your project. 123 | 124 | ***WARNING:*** This is a development tool. It can safely be used in a repository locally, since it is not used in production code. Commit and push changes before you start using. 125 | 126 | Don't forget: code like you're on 🔥 127 | 128 | The Netlify Logo is Copyright of [Netlify][2] and should not be used without their consent. 129 | 130 | [1]: https://github.com/ADARTA/netlify-cms-components/tree/master/packages/netlify-cms-starter 131 | [2]: https://www.netlify.com/ 132 | [3]: https://www.netlifycms.org/ 133 | [4]: https://github.com/ADARTA/netlify-cms-react-example 134 | [5]: https://github.com/ADARTA/gatsby-starter-netlify-cms 135 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netlify-cms-backend-fs", 3 | "version": "0.0.0-development", 4 | "description": "Backend for Netlify CMS to test with local file system", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/ADARTA/netlify-cms-components" 8 | }, 9 | "main": "dist/index.js", 10 | "files": [ 11 | "dist/", 12 | "src/", 13 | "README.md" 14 | ], 15 | "license": "MIT", 16 | "scripts": { 17 | "test:simple": "node ./scripts/_tests_/simple.js", 18 | "build:copy": "cp-cli scripts/fs/ dist/fs/", 19 | "build:lib": "cross-env NODE_ENV=production webpack-cli --mode=production", 20 | "build:dev": "cross-env NODE_ENV=development webpack-cli --mode=development", 21 | "build": "run-s build:copy build:lib", 22 | "dev": "run-s build:copy build:dev", 23 | "format": "prettier --write \"src/**/*.js\"", 24 | "prepublishOnly": "echo \"Create test and lint - (npm run test && npm run lint)\"", 25 | "version": "run-s format", 26 | "semantic-release:local": "semantic-release pre --debug=false && npm publish && semantic-release post --debug=false", 27 | "semantic-release": "semantic-release" 28 | }, 29 | "dependencies": { 30 | "body-parser": "^1.18.3", 31 | "chalk": "^2.4.1", 32 | "js-base64": "^2.5.0", 33 | "multer": "^1.4.1" 34 | }, 35 | "devDependencies": { 36 | "cp-cli": "2.0.0", 37 | "cross-env": "5.2.0", 38 | "semantic-release": "^15.13.3", 39 | "webpack-cli": "3.2.3" 40 | }, 41 | "peerDependencies": { 42 | "@emotion/core": "^10.0.9", 43 | "lodash": "^4.17.11", 44 | "react": "^16.8.0", 45 | "uuid": "^3.3.2" 46 | }, 47 | "keywords": [ 48 | "adarta", 49 | "netlify-cms", 50 | "backend", 51 | "fs" 52 | ], 53 | "author": "talves", 54 | "browserslist": [ 55 | "last 2 Chrome versions", 56 | "last 2 Opera versions", 57 | "last 2 Firefox versions", 58 | "last 2 Edge versions", 59 | "last 2 Safari versions", 60 | "last 2 iOS versions", 61 | "last 2 ChromeAndroid versions" 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/scripts/_tests_/simple.js: -------------------------------------------------------------------------------- 1 | const fsAPI = require('../fs/fs-api'); 2 | const { site, files, file } = fsAPI; 3 | 4 | site.setPath('src'); 5 | 6 | files('.').read(result => { 7 | result.map(item => { 8 | console.log(` ${item.name} [${item.path}]`); 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/scripts/fs/fs-api.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const chalk = require('chalk'); 4 | const log = console.log; 5 | 6 | const { version, name } = require('../../package.json'); 7 | const packageLabel = `[${name}]` 8 | 9 | log(chalk.green(`${packageLabel} (version: ${version})`)); 10 | 11 | const projectRoot = path.join(process.cwd()); 12 | log(chalk.green(`${packageLabel} root path is ${projectRoot}`)); 13 | 14 | const siteRoot = { 15 | dir: path.join(projectRoot, "example") 16 | }; 17 | const setPath = (relPath) => { 18 | siteRoot.dir = path.join(projectRoot, relPath); 19 | log(chalk.green(`${packageLabel} site path is ${ siteRoot.dir }`)); 20 | }; 21 | 22 | const logError = (message) => { 23 | log(chalk.red(message)); 24 | return new Error(message); 25 | } 26 | 27 | const notCallback = (cb) => { 28 | return (typeof cb !== "function") 29 | } 30 | 31 | module.exports = { 32 | site: { setPath }, 33 | files: (dirname) => { 34 | const name = "Files"; 35 | const read = (cb) => { 36 | if (notCallback(cb)) throw logError(`${packageLabel} [read] Error: missing callback`); 37 | const thispath = path.join(siteRoot.dir, dirname); 38 | const dirExists = fs.existsSync(thispath); 39 | if (!dirExists) log(chalk.yellow(`${packageLabel} [read] Warning: directory missing ${thispath}`)); 40 | const files = dirExists ? fs.readdirSync(thispath) : []; 41 | const filelist = []; 42 | files.forEach(function(element) { 43 | const filePath = path.join(thispath, element); 44 | const stats = fs.statSync(filePath); 45 | if (stats.isFile()) { 46 | filelist.push({ name: element, path: `${ dirname }/${ element }`, stats, type: "file" }); 47 | } 48 | }, this); 49 | cb(filelist); 50 | }; 51 | return { read, name }; 52 | }, 53 | file: (id) => { 54 | const name = "File"; 55 | const thisfile = path.join(siteRoot.dir, id); 56 | 57 | const readStats = (path) => { 58 | let stats; 59 | try { 60 | stats = fs.statSync(thisfile); 61 | } catch (err) { 62 | stats = {}; 63 | } 64 | return stats; 65 | } 66 | 67 | /* GET-Read an existing file */ 68 | const read = (cb) => { 69 | if (notCallback(cb)) throw logError(`${packageLabel} [read] Error: missing callback`); 70 | const stats = readStats(thisfile); 71 | if (typeof stats.isFile === "function" && stats.isFile()) { 72 | fs.readFile(thisfile, 'utf8', (err, data) => { 73 | if (err) { 74 | cb({ error: err }); 75 | } else { 76 | cb(data); 77 | } 78 | }); 79 | } else { 80 | cb({ error: logError(`${packageLabel} [read] Error: not a file(${thisfile})`) }); 81 | } 82 | }; 83 | /* POST-Create a NEW file, ERROR if exists */ 84 | const create = (body, cb) => { 85 | if (notCallback(cb)) throw logError(`${packageLabel} [create] Error: missing callback`); 86 | if (fs.existsSync(thisfile)) throw new Error(`${packageLabel} [create] Error: file exists (${thisfile})`); 87 | fs.writeFile(thisfile, body.content, { encoding: body.encoding, flag: 'wx' }, (err) => { 88 | if (err) { 89 | cb({ error: err }); 90 | } else { 91 | cb(body.content); 92 | } 93 | }); 94 | }; 95 | /* PUT-Update an existing file */ 96 | const update = (body, cb) => { 97 | if (notCallback(cb)) throw logError(`${packageLabel} [update] Error: missing callback`); 98 | if (!fs.existsSync(thisfile)) throw new Error(`${packageLabel} [update] Error: file does not exist (${thisfile})`); 99 | fs.writeFile(thisfile, body.content, { encoding: body.encoding, flag: 'w' }, (err) => { 100 | if (err) { 101 | cb({ error: err }); 102 | } else { 103 | cb(body.content); 104 | } 105 | }); 106 | }; 107 | /* DELETE an existing file */ 108 | const del = (cb) => { 109 | if (notCallback(cb)) throw logError(`${packageLabel} [del] Error: missing callback`); 110 | fs.unlink(thisfile, (err) => { 111 | if (err) { 112 | cb({ error: err }); 113 | } else { 114 | cb(`${packageLabel} deleted file (${ thisfile })`); 115 | } 116 | }); 117 | }; 118 | return { read, create, update, del }; 119 | }, 120 | }; 121 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/scripts/fs/fs-express-api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is to be used by node. There is no file system access at the client. 3 | * To be called from our webpack devServer config.before 4 | * See: http://expressjs.com/en/guide/using-middleware.html 5 | * 6 | * const fsExpressAPI = require('netlify-cms-backend-fs/dist/scripts/fs/fs-express-api'); 7 | * // CRA v2 using the ./src/setupProxy.js 8 | * module.exports = fsExpressAPI; 9 | * 10 | * // webpack v4.x devServer config 11 | * module.exports = { 12 | * //..... 13 | * devServer: { 14 | * before: fsExpressAPI, 15 | * }, 16 | * } 17 | **/ 18 | const path = require('path'); 19 | const bodyParser = require('body-parser'); 20 | const multer = require('multer'); 21 | const pkg = require(path.join(process.cwd(), 'package.json')); 22 | const fsAPI = require('./fs-api'); 23 | const fsPath = pkg.fileSystemPath || ''; 24 | 25 | /* Express allows for app object setup to handle paths (our api routes) */ 26 | module.exports = function(app) { 27 | fsAPI.site.setPath(fsPath); 28 | const upload = multer(); // for parsing multipart/form-data 29 | const uploadLimit = '50mb'; // express has a default of ~20Kb 30 | app.use(bodyParser.json({ limit: uploadLimit })); // for parsing application/json 31 | app.use(bodyParser.urlencoded({ limit: uploadLimit, extended: true, parameterLimit:50000 })); // for parsing application/x-www-form-urlencoded 32 | 33 | // We will look at every route to bypass any /api route from the react app 34 | app.use('/:path', function(req, res, next) { 35 | // if the path is api, skip to the next route 36 | if (req.params.path === 'api') { 37 | next('route'); 38 | } 39 | // otherwise pass the control out of this middleware to the next middleware function in this stack (back to regular) 40 | else next(); 41 | }); 42 | 43 | app.use('/api', function(req, res, next) { 44 | const response = { route: '/api', url: req.originalUrl }; 45 | if (req.originalUrl === "/api" || req.originalUrl === "/api/") { 46 | // if the requested url is the root, , respond Error! 47 | response.status = 500; 48 | response.error = 'This is the root of the API'; 49 | res.status(response.status).json(response); 50 | } else { 51 | // continue to the next sub-route ('/api/:path') 52 | next('route'); 53 | } 54 | }); 55 | 56 | /* Define custom handlers for api paths: */ 57 | app.use('/api/:path', function(req, res, next) { 58 | const response = { route: '/api/:path', path: req.params.path, params: req.params }; 59 | if (req.params.path && req.params.path in fsAPI) { 60 | // all good, route exists in the api 61 | next('route'); 62 | } else { 63 | // sub-route was not found in the api, respond Error! 64 | response.status = 500; 65 | response.error = `Invalid path ${ req.params.path }`; 66 | res.status(response.status).json(response); 67 | } 68 | }); 69 | 70 | /* Files */ 71 | 72 | /* Return all the files in the starting path */ 73 | app.get('/api/files', function(req, res, next) { 74 | const response = { route: '/api/files' }; 75 | try { 76 | fsAPI.files('./').read((contents) => { 77 | res.json(contents); 78 | }); 79 | } catch (err) { 80 | response.status = 500; 81 | response.error = `Could not get files - code [${ err.code }]`; 82 | response.internalError = err; 83 | res.status(response.status).send(response); 84 | } 85 | }); 86 | 87 | /* Return all the files in the passed path */ 88 | app.get('/api/files/:path', function(req, res, next) { 89 | const response = { route: '/api/files/:path', params: req.params, path: req.params.path }; 90 | try { 91 | fsAPI.files(req.params.path).read((contents) => { 92 | res.json(contents); 93 | }); 94 | } catch (err) { 95 | response.status = 500; 96 | response.error = `Could not get files for ${ req.params.path } - code [${ err.code }]`; 97 | response.internalError = err; 98 | res.status(response.status).send(response); 99 | } 100 | }); 101 | /* Capture Unknown extras and handle path (ignore?) */ 102 | app.get('/api/files/:path/**', function(req, res, next) { 103 | const response = { route: '/api/files/:path/**', params: req.params, path: req.params.path }; 104 | const filesPath = req.originalUrl.substring(11, req.originalUrl.split('?', 1)[0].length); 105 | try { 106 | fsAPI.files(filesPath).read((contents) => { 107 | res.json(contents); 108 | }); 109 | } catch (err) { 110 | response.status = 500; 111 | response.error = `Could not get files for ${ filesPath } - code [${ err.code }]`; 112 | response.internalError = err; 113 | res.status(response.status).send(response); 114 | } 115 | }); 116 | 117 | /* File */ 118 | 119 | app.get('/api/file', function(req, res, next) { 120 | const response = { error: 'Id cannot be empty for file', status: 500, path: res.path }; 121 | res.status(response.status).send(response); 122 | }); 123 | 124 | app.get('/api/file/:id', function(req, res, next) { 125 | const response = { route: '/api/file/:id', id: req.params.id }; 126 | const allDone = (contents) => { 127 | if (contents.error) { 128 | response.status = 500; 129 | response.error = `Could not read file ${ req.params.id } - code [${ contents.error.code }]`; 130 | response.internalError = contents.error; 131 | res.status(response.status).send(response); 132 | } else { 133 | res.json(contents); 134 | } 135 | }; 136 | if (req.params.id) { 137 | fsAPI.file(req.params.id).read(allDone); 138 | } else { 139 | response.status = 500; 140 | response.error = `Invalid id for File ${ req.params.id }`; 141 | res.status(response.status).send(response); 142 | } 143 | }); 144 | /* Capture Unknown extras and ignore the rest */ 145 | app.get('/api/file/:id/**', function(req, res, next) { 146 | const response = { route: '/api/file/:id', id: req.params.id, method:req.method }; 147 | const filePath = req.originalUrl.substring(10, req.originalUrl.split('?', 1)[0].length); 148 | const allDone = (contents) => { 149 | if (contents.error) { 150 | response.status = 500; 151 | response.error = `Could not read file ${ filePath } - code [${ contents.error.code }]`; 152 | response.internalError = contents.error; 153 | res.status(response.status).send(response); 154 | } else { 155 | res.json(contents); 156 | } 157 | }; 158 | if (filePath) { 159 | fsAPI.file(filePath).read(allDone); 160 | } else { 161 | response.status = 500; 162 | response.error = `Invalid path for File ${ filePath }`; 163 | res.status(response.status).send(response); 164 | } 165 | }); 166 | /* Create file if path does not exist */ 167 | app.post('/api/file/:id/**', upload.array(), function(req, res, next) { 168 | const response = { route: '/api/file/:id', id: req.params.id, method:req.method }; 169 | const filePath = req.originalUrl.substring(10, req.originalUrl.split('?', 1)[0].length); 170 | const allDone = (contents) => { 171 | if (contents.error) { 172 | response.status = 500; 173 | response.error = `Could not create file ${ filePath } - code [${ contents.error.code }]`; 174 | response.internalError = contents.error; 175 | res.status(response.status).send(response); 176 | } else { 177 | res.json(contents); 178 | } 179 | }; 180 | if (filePath) { 181 | fsAPI.file(filePath).create(req.body, allDone); 182 | } else { 183 | response.status = 500; 184 | response.error = `Invalid path for File ${ filePath }`; 185 | res.status(response.status).send(response); 186 | } 187 | }); 188 | /* Update file, error on path exists */ 189 | app.put('/api/file/:id/**', upload.array(), function(req, res, next) { 190 | const response = { route: '/api/file/:id', id: req.params.id, method:req.method }; 191 | const filePath = req.originalUrl.substring(10, req.originalUrl.split('?', 1)[0].length); 192 | const allDone = (contents) => { 193 | if (contents.error) { 194 | response.status = 500; 195 | response.error = `Could not update file ${ filePath } - code [${ contents.error.code }]`; 196 | response.internalError = contents.error; 197 | res.status(response.status).send(response); 198 | } else { 199 | res.json(contents); 200 | } 201 | }; 202 | if (filePath) { 203 | fsAPI.file(filePath).update(req.body, allDone); 204 | } else { 205 | response.status = 500; 206 | response.error = `Invalid path for File ${ filePath }`; 207 | res.status(response.status).send(response); 208 | } 209 | }); 210 | /* Delete file, error if no file */ 211 | app.delete('/api/file/:id/**', function(req, res, next) { 212 | const response = { route: '/api/file/:id', id: req.params.id, method:req.method }; 213 | const filePath = req.originalUrl.substring(10, req.originalUrl.split('?', 1)[0].length); 214 | const allDone = (contents) => { 215 | if (contents.error) { 216 | response.status = 500; 217 | response.error = `Could not delete file ${ filePath } - code [${ contents.error.code }]`; 218 | response.internalError = contents.error; 219 | res.status(response.status).send(response); 220 | } else { 221 | res.json(contents); 222 | } 223 | }; 224 | if (filePath) { 225 | fsAPI.file(filePath).del(allDone); 226 | } else { 227 | response.status = 500; 228 | response.error = `Invalid path for File ${ filePath }`; 229 | res.status(response.status).send(response); 230 | } 231 | }); 232 | }; 233 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/scripts/fs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./fs-express-api.js'); -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/src/API.js: -------------------------------------------------------------------------------- 1 | import { Base64 } from 'js-base64'; 2 | import APIError from './lib/APIError'; 3 | const SIMPLE = 'simple'; 4 | 5 | export default class API { 6 | constructor(config) { 7 | this.api_root = config.api_root || '/api'; 8 | } 9 | 10 | user() { 11 | return this.request('/user'); 12 | } 13 | 14 | requestHeaders(headers = {}) { 15 | const baseHeader = { 16 | 'Content-Type': 'application/json', 17 | ...headers, 18 | }; 19 | 20 | return baseHeader; 21 | } 22 | 23 | parseJsonResponse(response) { 24 | return response.json().then(json => { 25 | if (!response.ok) { 26 | return Promise.reject(json); 27 | } 28 | 29 | return json; 30 | }); 31 | } 32 | 33 | urlFor(path, options) { 34 | const cacheBuster = new Date().getTime(); 35 | const params = [`ts=${cacheBuster}`]; 36 | if (options.params) { 37 | for (const key in options.params) { 38 | params.push(`${key}=${encodeURIComponent(options.params[key])}`); 39 | } 40 | } 41 | if (params.length) { 42 | path += `?${params.join('&')}`; 43 | } 44 | return this.api_root + path; 45 | } 46 | 47 | request(path, options = {}) { 48 | const headers = this.requestHeaders(options.headers || {}); 49 | const url = this.urlFor(path, options); 50 | let responseStatus; 51 | return fetch(url, { ...options, headers }) 52 | .then(response => { 53 | responseStatus = response.status; 54 | const contentType = response.headers.get('Content-Type'); 55 | if (contentType && contentType.match(/json/)) { 56 | return this.parseJsonResponse(response); 57 | } 58 | return response.text(); 59 | }) 60 | .catch(error => { 61 | throw new APIError(error.message, responseStatus, 'fs'); 62 | }); 63 | } 64 | 65 | readFile(path) { 66 | const cache = Promise.resolve(null); 67 | return cache.then(cached => { 68 | if (cached) { 69 | return cached; 70 | } 71 | 72 | return this.request(`/file/${path}`, { 73 | headers: { Accept: 'application/octet-stream' }, 74 | params: {}, 75 | cache: 'no-store', 76 | }).then(result => { 77 | return result; 78 | }); 79 | }); 80 | } 81 | 82 | listFiles(path) { 83 | return this.request(`/files/${path}`, { 84 | params: {}, 85 | }) 86 | .then(files => { 87 | if (!Array.isArray(files)) { 88 | throw new Error(`Cannot list files, path ${path} is not a directory but a ${files.type}`); 89 | } 90 | return files; 91 | }) 92 | .then(files => files.filter(file => file.type === 'file')); 93 | } 94 | 95 | composeFileTree(files) { 96 | let filename; 97 | let part; 98 | let parts; 99 | let subtree; 100 | const fileTree = {}; 101 | 102 | files.forEach(file => { 103 | if (file.uploaded) { 104 | return; 105 | } 106 | parts = file.path.split('/').filter(part => part); 107 | filename = parts.pop(); 108 | subtree = fileTree; 109 | while (part === parts.shift()) { 110 | subtree[part] = subtree[part] || {}; 111 | subtree = subtree[part]; 112 | } 113 | subtree[filename] = file; 114 | file.file = true; 115 | }); 116 | 117 | return fileTree; 118 | } 119 | 120 | toBase64(str) { 121 | return Promise.resolve(Base64.encode(str)); 122 | } 123 | 124 | uploadBlob(item, newFile = false) { 125 | const content = item.raw ? this.toBase64(item.raw) : item.toBase64(); 126 | const method = newFile ? 'POST' : 'PUT'; // Always update or create new. PUT is Update existing only 127 | 128 | const pathID = item.path.substring(0, 1) === '/' ? item.path.substring(1, item.path.length) : item.path.toString(); 129 | 130 | return content.then(contentBase64 => 131 | this.request(`/file/${pathID}`, { 132 | method: method, 133 | body: JSON.stringify({ 134 | content: contentBase64, 135 | encoding: 'base64', 136 | }), 137 | }).then(response => { 138 | item.sha = response.sha; 139 | item.uploaded = true; 140 | return item; 141 | }), 142 | ); 143 | } 144 | 145 | persistFiles(entry, mediaFiles, options) { 146 | const uploadPromises = []; 147 | const files = entry ? mediaFiles.concat(entry) : mediaFiles; 148 | 149 | files.forEach(file => { 150 | if (file.uploaded) { 151 | return; 152 | } 153 | uploadPromises.push(this.uploadBlob(file, options.newEntry || (file.toBase64 && !file.raw))); 154 | }); 155 | 156 | const fileTree = this.composeFileTree(files); 157 | 158 | return Promise.all(uploadPromises).then(() => { 159 | if (!options.mode || (options.mode && options.mode === SIMPLE)) { 160 | return fileTree; 161 | } 162 | }); 163 | } 164 | 165 | deleteFile(path, message, options = {}) { 166 | const fileURL = `/file/${path}`; 167 | return this.request(fileURL, { 168 | method: 'DELETE', 169 | params: {}, 170 | }); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/src/AuthenticationPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Authentication from './components/Authentication'; 3 | 4 | const defaultUser = { email: 'developer@localhost.com' } 5 | 6 | function AuthenticationPage({ config, onLogin, inProgress }) { 7 | const [user, setUser] = React.useState(defaultUser) 8 | const [loggingIn, setLoggingIn] = React.useState(inProgress) 9 | const logoPath = config.get('logo_url') || '' 10 | const skipLogin = config.getIn(['backend', 'login']) === false 11 | 12 | const handleLogin = event => { 13 | event.preventDefault() 14 | onLogin(user) 15 | } 16 | 17 | React.useEffect(() => { 18 | if (skipLogin) onLogin(defaultUser) 19 | }) 20 | 21 | React.useEffect(() => { 22 | setLoggingIn(inProgress) 23 | if (inProgress) setUser(defaultUser) 24 | }, [inProgress]) 25 | 26 | return 31 | } 32 | 33 | export default AuthenticationPage 34 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/src/components/Authentication.js: -------------------------------------------------------------------------------- 1 | /** @jsx jsx */ 2 | import React from 'react'; 3 | import { css, jsx } from '@emotion/core'; 4 | import LoginButtonIcon from './FolderIcon'; 5 | 6 | const shadows = { 7 | drop: css` 8 | box-shadow: 0 2px 4px 0 rgba(19, 39, 48, 0.12); 9 | `, 10 | dropDeep: css` 11 | box-shadow: 0 4px 12px 0 rgba(68, 74, 87, 0.15), 0 1px 3px 0 rgba(68, 74, 87, 0.25); 12 | `, 13 | }; 14 | 15 | const buttons = { 16 | button: css` 17 | border: 0; 18 | border-radius: 5px; 19 | cursor: pointer; 20 | `, 21 | default: css` 22 | height: 36px; 23 | line-height: 36px; 24 | font-weight: 500; 25 | padding: 0 15px; 26 | background-color: #798291; 27 | color: #fff; 28 | `, 29 | gray: css` 30 | background-color: #798291; 31 | color: #fff; 32 | 33 | &:focus, 34 | &:hover { 35 | color: #fff; 36 | background-color: #555a65; 37 | } 38 | `, 39 | disabled: css` 40 | background-color: #eff0f4; 41 | color: #798291; 42 | `, 43 | }; 44 | 45 | const pageCss = css` 46 | display: flex; 47 | flex-flow: column nowrap; 48 | align-items: center; 49 | justify-content: center; 50 | height: 100vh; 51 | ` 52 | 53 | const wrapperCss = css` 54 | width: 300px; 55 | height: 200px; 56 | margin-top: -150px; 57 | ` 58 | 59 | function StyledAuthenticationPage({ children }) { 60 | return ( 61 |
62 | {children} 63 |
64 | ) 65 | } 66 | 67 | function renderPageLogo({ logoUrl }) { 68 | if (logoUrl) { 69 | return ( 70 | 71 | Logo 72 | 73 | ) 74 | } 75 | return null 76 | } 77 | 78 | const buttonCss = css` 79 | padding: 0 12px; 80 | margin-top: -40px; 81 | display: flex; 82 | align-items: center; 83 | position: relative; 84 | ` 85 | const buttonLogoCss = css` 86 | width: 24px; 87 | height: 24px; 88 | ` 89 | 90 | function LoginLabel({ progress }) { 91 | 92 | return ( 93 | 94 | {progress ? 'Logging in...' : 'Login to File System'} 95 | 96 | ) 97 | } 98 | 99 | function Authentication({ 100 | onLogin, 101 | loginStatus, 102 | loginErrorMessage, 103 | logoUrl, 104 | }) { 105 | const [status, setStatus] = React.useState(loginStatus) 106 | 107 | React.useEffect(() => { 108 | setStatus(loginStatus) 109 | }, [loginStatus]) 110 | 111 | return ( 112 | 113 | {renderPageLogo(logoUrl)} 114 | {loginErrorMessage ?

{loginErrorMessage}

: null} 115 | 129 |
130 | ); 131 | } 132 | 133 | export default Authentication; 134 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/src/components/FolderIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function FolderIcon() { 4 | return () 5 | } -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/src/implementation.js: -------------------------------------------------------------------------------- 1 | import trimStart from 'lodash/trimStart'; 2 | import uuid from 'uuid/v4'; 3 | import AuthenticationPage from './AuthenticationPage'; 4 | import API from './API'; 5 | import { fileExtension } from './lib/pathHelper'; 6 | 7 | const nameFromEmail = email => { 8 | return email 9 | .split('@') 10 | .shift() 11 | .replace(/[.-_]/g, ' ') 12 | .split(' ') 13 | .filter(f => f) 14 | .map(s => s.substr(0, 1).toUpperCase() + (s.substr(1) || '')) 15 | .join(' '); 16 | }; 17 | 18 | export class FileSystemBackend { 19 | constructor(config) { 20 | this.config = config; 21 | 22 | this.api_root = config.getIn(['backend', 'api_root'], 'http://localhost:8080/api'); 23 | console.log(`Setting up file-system backend api: ${this.api_root}`); 24 | if (FILESYSTEMBACKEND_VERSION) { 25 | console.log(FILESYSTEMBACKEND_VERSION); 26 | } 27 | } 28 | 29 | authComponent() { 30 | return AuthenticationPage; 31 | } 32 | 33 | restoreUser(user) { 34 | return this.authenticate(user); 35 | } 36 | 37 | authenticate(state) { 38 | this.api = new API({ api_root: this.api_root }); 39 | return Promise.resolve({ email: state.email, name: nameFromEmail(state.email) }); 40 | } 41 | 42 | logout() { 43 | return null; 44 | } 45 | 46 | getToken() { 47 | return Promise.resolve(''); 48 | } 49 | 50 | entriesByFolder(collection, extension) { 51 | return this.api 52 | .listFiles(collection.get('folder')) 53 | .then(files => files.filter(file => fileExtension(file.name) === extension)) 54 | .then(files => this.fetchFiles(files)); 55 | } 56 | 57 | entriesByFiles(collection) { 58 | const files = collection.get('files').map(collectionFile => ({ 59 | path: collectionFile.get('file'), 60 | label: collectionFile.get('label'), 61 | })); 62 | return this.fetchFiles(files); 63 | } 64 | 65 | fetchFiles(files) { 66 | const api = this.api; 67 | const promises = []; 68 | files.forEach(file => { 69 | promises.push( 70 | new Promise((resolve, reject) => 71 | api.readFile(file.path) 72 | .then(data => { 73 | resolve({ file, data }); 74 | }) 75 | .catch(err => { 76 | reject(err); 77 | }) 78 | ) 79 | ); 80 | }); 81 | return Promise.all(promises); 82 | } 83 | 84 | getEntry(collection, slug, path) { 85 | return this.api.readFile(path).then(data => ({ 86 | file: { path }, 87 | data, 88 | })); 89 | } 90 | 91 | getMedia() { 92 | const publicFolderPath = this.config.get('public_folder') || ''; 93 | return this.api 94 | .listFiles(this.config.get('media_folder')) 95 | .then(files => files.filter(file => file.type === 'file')) 96 | .then(files => 97 | files.map(({ name, stats, path }) => { 98 | return { 99 | id: uuid(), 100 | name, 101 | size: stats.size, 102 | urlIsPublicPath: false, 103 | displayURL: `${publicFolderPath}/${name}`, 104 | url: `${publicFolderPath}/${name}`, 105 | path, 106 | }; 107 | }), 108 | ); 109 | } 110 | 111 | persistEntry(entry, mediaFiles = [], options = {}) { 112 | return this.api.persistFiles(entry, mediaFiles, options); 113 | } 114 | 115 | async persistMedia(mediaFile, options = {}) { 116 | try { 117 | await this.api.persistFiles(null, [mediaFile], options); 118 | 119 | const { sha, value, path, fileObj } = mediaFile; 120 | const displayURL = URL.createObjectURL(fileObj); 121 | return { 122 | id: sha || `backend-fs-${value}`, 123 | name: value, 124 | size: fileObj.size, 125 | displayURL, 126 | path: trimStart(path, '/') }; 127 | } catch (error) { 128 | console.error(error); 129 | throw error; 130 | } 131 | } 132 | 133 | deleteFile(path, commitMessage, options) { 134 | return this.api.deleteFile(path, commitMessage, options); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/src/lib/APIError.js: -------------------------------------------------------------------------------- 1 | export const API_ERROR = 'API_ERROR'; 2 | 3 | export default class APIError extends Error { 4 | constructor(message, status, api) { 5 | super(message); 6 | this.message = message; 7 | this.status = status; 8 | this.api = api; 9 | this.name = API_ERROR; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/src/lib/pathHelper.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | const normalizePath = path => path.replace(/[\\\/]+/g, '/'); 3 | 4 | /** 5 | * Return the extension of the path, from the last '.' to end of string in the 6 | * last portion of the path. If there is no '.' in the last portion of the path 7 | * or the first character of it is '.', then it returns an empty string. 8 | * @example Usage example 9 | * path.fileExtensionWithSeparator('index.html') 10 | * // returns 11 | * '.html' 12 | */ 13 | export function fileExtensionWithSeparator(p) { 14 | p = normalizePath(p); 15 | const sections = p.split('/'); 16 | p = sections.pop(); 17 | // Special case: foo/file.ext/ should return '.ext' 18 | if (p === '' && sections.length > 0) { 19 | p = sections.pop(); 20 | } 21 | if (p === '..') { 22 | return ''; 23 | } 24 | const i = p.lastIndexOf('.'); 25 | if (i === -1 || i === 0) { 26 | return ''; 27 | } 28 | return p.substr(i); 29 | } 30 | 31 | /** 32 | * Return the extension of the path, from after the last '.' to end of string in the 33 | * last portion of the path. If there is no '.' in the last portion of the path 34 | * or the first character of it is '.', then it returns an empty string. 35 | * @example Usage example 36 | * path.fileExtension('index.html') 37 | * // returns 38 | * 'html' 39 | */ 40 | export function fileExtension(p) { 41 | const ext = fileExtensionWithSeparator(p); 42 | return ext === '' ? ext : ext.substr(1); 43 | } 44 | -------------------------------------------------------------------------------- /packages/netlify-cms-backend-fs/webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * webpack.config.js 3 | */ 4 | const path = require('path'); 5 | const webpack = require('webpack'); 6 | const pkg = require(path.join(process.cwd(), 'package.json')); 7 | 8 | const externals = { 9 | '@emotion/core': { 10 | root: ['NetlifyCmsDefaultExports', 'EmotionCore'], 11 | commonjs2: '@emotion/core', 12 | commonjs: '@emotion/core', 13 | amd: '@emotion/core', 14 | umd: '@emotion/core', 15 | }, 16 | lodash: { 17 | root: ['NetlifyCmsDefaultExports', 'Lodash'], 18 | commonjs2: 'lodash', 19 | commonjs: 'lodash', 20 | amd: 'lodash', 21 | umd: 'lodash', 22 | }, 23 | react: { 24 | root: 'React', 25 | commonjs2: 'react', 26 | commonjs: 'react', 27 | amd: 'react', 28 | umd: 'react', 29 | }, 30 | uuid: { 31 | root: ['NetlifyCmsDefaultExports', 'UUId'], 32 | commonjs2: 'uuid', 33 | commonjs: 'uuid', 34 | amd: 'uuid', 35 | umd: 'uuid', 36 | }, 37 | } 38 | 39 | const baseConfig = { 40 | module: { 41 | rules: [ 42 | { 43 | test: /\.m?js$/, 44 | exclude: /(node_modules)/, 45 | use: { 46 | loader: 'babel-loader', 47 | options: { 48 | rootMode: "upward", 49 | } 50 | } 51 | }, { 52 | test: /\.m?jsx$/, 53 | exclude: /(node_modules)/, 54 | use: { 55 | loader: 'babel-loader', 56 | options: { 57 | rootMode: "upward", 58 | } 59 | } 60 | }, { 61 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, 62 | exclude: /(node_modules)/, 63 | use: { 64 | loader: 'svg-inline-loader' 65 | } 66 | } 67 | ] 68 | }, 69 | plugins: [], 70 | } 71 | 72 | const defaultConfig = { 73 | ...baseConfig, 74 | entry: { 75 | 'index': './src/implementation.js' 76 | }, 77 | output: { 78 | path: path.join(__dirname, 'dist'), 79 | filename: '[name].js', 80 | library: 'FileSystemBackendClass', 81 | libraryTarget:'umd', 82 | libraryExport: 'FileSystemBackend', 83 | umdNamedDefine: true, 84 | }, 85 | /** 86 | * Exclude peer dependencies from package bundles. 87 | */ 88 | externals, 89 | } 90 | 91 | module.exports = (env, argv) => { 92 | const isProduction = (argv.mode === 'production'); 93 | defaultConfig.devtool = 'source-map'; 94 | 95 | const versionPlugin = new webpack.DefinePlugin({ 96 | FILESYSTEMBACKEND_VERSION: JSON.stringify(`${pkg.name} v${pkg.version}${isProduction ? '' : '-dev'}`), 97 | }); 98 | defaultConfig.plugins.push(versionPlugin); 99 | 100 | return [defaultConfig]; 101 | } 102 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/README.md: -------------------------------------------------------------------------------- 1 | This Starter (bootstrap) project shows how the NetlifyCMS can be extended using custom widgets 2 | 3 | - Uses Yarn and Lerna to scaffold up the project in one mono-repo 4 | - See `packages/netlify-cms-starter` for main NetlifyCMS starter project 5 | 6 | ***NOTE:*** 7 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). 8 | You can find the most recent version of the guide [here](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md). 9 | 10 | For a more complete example with file-system writes into your repository during dev, clone [this repo][example] 11 | 12 | Always code like you are on 🔥 13 | 14 | [example]: https://github.com/adarta/netlify-cms-react-example/tree/master 15 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/example/recipes.json: -------------------------------------------------------------------------------- 1 | { 2 | "items": [ 3 | { 4 | "title": "Almond Roca1", 5 | "id": "roca", 6 | "directions": "- Item 1\n- Item 2" 7 | }, 8 | { 9 | "title": "This is Recipe Two", 10 | "id": "number2", 11 | "directions": "- Item 1\n- Item 2" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /packages/netlify-cms-starter/example/test.yml: -------------------------------------------------------------------------------- 1 | test_widget: '2' 2 | test_widget_number: Testing the value 3 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "netlify-cms-starter", 3 | "version": "0.3.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "react-scripts start", 7 | "build": "react-scripts build", 8 | "test": "react-scripts test --env=jsdom", 9 | "eject": "react-scripts eject" 10 | }, 11 | "dependencies": { 12 | "netlify-cms-app": "^2.9.0", 13 | "react": "^16.8.4", 14 | "react-dom": "^16.8.4" 15 | }, 16 | "devDependencies": { 17 | "eslint": "5.12.0", 18 | "lodash.flow": "^3.5.0", 19 | "react-scripts": "^2.1.8" 20 | }, 21 | "browserslist": [ 22 | "last 2 Chrome versions", 23 | "last 2 Opera versions", 24 | "last 2 Firefox versions", 25 | "last 2 Edge versions", 26 | "last 2 Safari versions", 27 | "last 2 iOS versions", 28 | "last 2 ChromeAndroid versions" 29 | ], 30 | "fileSystemPath": "" 31 | } 32 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/public/assets/media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Netlify 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADARTA/netlify-cms-components/ab5d287d6a7337b9ee562dbf96fae277f3f18248/packages/netlify-cms-starter/public/favicon.ico -------------------------------------------------------------------------------- /packages/netlify-cms-starter/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/public/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ADARTA/netlify-cms-components/ab5d287d6a7337b9ee562dbf96fae277f3f18248/packages/netlify-cms-starter/public/uploads/.gitkeep -------------------------------------------------------------------------------- /packages/netlify-cms-starter/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import NetlifyCMS from './components/NetlifyCMS'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 | 9 | ); 10 | } 11 | } 12 | 13 | export default App; 14 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/src/components/NetlifyCMS/components/AuthorsPreview/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default class AuthorsPreview extends React.Component { 4 | render () { 5 | return ( 6 |
7 |

Authors

8 | { 9 | this.props.widgetsFor('authors').map( (author, index) => { 10 | return ( 11 |
12 |
13 | { author.getIn(['data', 'name']) } 14 | { author.getIn(['widgets', 'description']) } 15 |
16 | ) 17 | }) 18 | } 19 |
20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /packages/netlify-cms-starter/src/components/NetlifyCMS/components/EditorYoutube/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | id: 'youtube', 3 | label: 'Youtube', 4 | fields: [{ name: 'id', label: 'Youtube Video ID' }], 5 | pattern: /^{{<\s?youtube (\S+)\s?>}}/, 6 | fromBlock: match => ({ 7 | id: match[1], 8 | }), 9 | toBlock: obj => `{{< youtube ${obj.id} >}}`, 10 | toPreview: obj => `Youtube Video`, 11 | } 12 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/src/components/NetlifyCMS/data/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "backend": { 3 | "name": "test-repo" 4 | }, 5 | "load_config_file": false, 6 | "display_url": "https://netlify-cms-components.netlify.com", 7 | "media_folder": "public/uploads", 8 | "public_folder": "/uploads", 9 | "collections": [{ 10 | "name": "test", 11 | "label": "Test", 12 | "files": [{ 13 | "label": "Test", 14 | "name": "test", 15 | "file": "example/test.yml", 16 | "fields": [{ 17 | "name": "test_widget", 18 | "label": "Test Number", 19 | "widget": "number" 20 | }, 21 | { 22 | "name": "test_widget_number", 23 | "label": "Test String", 24 | "widget": "string", 25 | "required": false 26 | } 27 | ] 28 | }, { 29 | "label": "Recipes", 30 | "name": "recipes", 31 | "file": "example/recipes.json", 32 | "fields": [{ 33 | "name": "items", 34 | "label": "Recipe", 35 | "widget": "list", 36 | "fields": [{ 37 | "name": "title", 38 | "label": "Recipe Title", 39 | "widget": "string" 40 | }, 41 | { 42 | "name": "id", 43 | "label": "Recipe Id", 44 | "widget": "string" 45 | }, 46 | { 47 | "name": "directions", 48 | "label": "Recipe Directions", 49 | "widget": "markdown" 50 | } 51 | ] 52 | }] 53 | }] 54 | }] 55 | } -------------------------------------------------------------------------------- /packages/netlify-cms-starter/src/components/NetlifyCMS/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CMS from 'netlify-cms-app'; 3 | import FileSystemBackend from 'netlify-cms-backend-fs'; 4 | 5 | import config from './data/config.json'; 6 | import AuthorsPreview from './components/AuthorsPreview'; 7 | import EditorYoutube from './components/EditorYoutube'; 8 | 9 | function NetlifyCMS() { 10 | React.useEffect(() => { 11 | if (process.env.NODE_ENV === 'development') { 12 | // const FileSystemBackend = import('netlify-cms-backend-fs'); 13 | // console.log('FileSystemBackend', FileSystemBackend) 14 | config.backend = { 15 | "name": "file-system", 16 | "api_root": "http://localhost:3000/api" 17 | } 18 | CMS.registerBackend('file-system', FileSystemBackend); 19 | } 20 | CMS.registerPreviewTemplate('authors', AuthorsPreview); 21 | CMS.registerEditorComponent(EditorYoutube); 22 | 23 | CMS.init({ config }); 24 | }) 25 | 26 | return ( 27 |
28 | ); 29 | } 30 | 31 | export default NetlifyCMS; 32 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: http://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /packages/netlify-cms-starter/src/setupProxy.js: -------------------------------------------------------------------------------- 1 | // https://facebook.github.io/create-react-app/docs/proxying-api-requests-in-development#configuring-the-proxy-manually 2 | const fsExpressAPI = require('netlify-cms-backend-fs/dist/fs'); 3 | 4 | module.exports = fsExpressAPI; 5 | --------------------------------------------------------------------------------