├── .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 | [](https://travis-ci.org/ADARTA/netlify-cms-components)
6 | [](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 |
64 | )
65 | }
66 |
67 | function renderPageLogo({ logoUrl }) {
68 | if (logoUrl) {
69 | return (
70 |
71 |
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 |
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 => `
`,
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 |
--------------------------------------------------------------------------------