diff --git a/.eleventy.js b/.eleventy.js index d35683b..4dcae32 100644 --- a/.eleventy.js +++ b/.eleventy.js @@ -42,7 +42,7 @@ module.exports = function (eleventyConfig, defaultOptions) { log(`⭐️ Starting ${blue('eleventy-plugin-cloudcannon')}`); const context = ctx.getAll(); const config = readConfig(context, options); - const info = getInfo(context, config, options); + const info = getInfo(context, config); const json = stringifyJson(info); log(`🏁 Generated ${bold('_cloudcannon/info.json')} ${green('successfully')}`); return json; diff --git a/.eslintrc.json b/.eslintrc.json index 654a711..4ca260a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,7 +3,15 @@ "eslint:recommended" ], "rules": { - "quotes": ["warn", "single"] + "indent": ["warn", "tab"], + "no-else-return": ["warn"], + "guard-for-in": ["warn"], + "eqeqeq": ["warn"], + "no-self-compare": ["warn"], + "no-duplicate-imports": ["warn"], + "no-lonely-if": ["warn"], + "quotes": ["warn", "single"], + "semi": ["warn", "always"] }, "env": { "es6": true, diff --git a/README.md b/README.md index 2d88ca9..70af590 100644 --- a/README.md +++ b/README.md @@ -8,25 +8,42 @@ An Eleventy (11ty) plugin that creates [CloudCannon](https://cloudcannon.com/) b *** - [Installation](#installation) +- [Configuration](#configuration) - [Plugin options](#plugin-options) -- [Site configuration](#site-configuration) -- [Manual installation](#manual-installation) - [License](#license) *** ## Installation -CloudCannon automatically injects this plugin before your site is built. This means you'll get new -features and fixes as they are released. You can [disable this](#manual-installation) if you need -to maintain the plugin versioning manually. +**You don't have to install anything** when building on CloudCannon. This plugin is automatically +installed before your site is built. This gives you the latest support, new features, and fixes +as they are released. -If you use custom paths for your site, you'll need to pass them to the plugin, either one of: +Although **not recommended**, you can disable the automatic installation and install the plugin +manually. -- Returning from `.eleventy.js` -- Setting `eleventyConfig.cloudcannonOptions` +
+Manual installation steps + +
+ +When installing manually, you'll have to upgrade when new versions are released. +You could also follow these steps to debug an integration issue locally. + +Start by enabling the "Manage eleventy-plugin-cloudcannon plugin manually" option in CloudCannon +for your site in *Site Settings / Build*. + +```bash +npm install eleventy-plugin-cloudcannon --save +``` + +Add the following `addPlugin` call to your `.eleventy.js` file. +The second parameter is optional, and used to pass [plugin options](#plugin-options). ```javascript +const pluginCloudCannon = require('eleventy-plugin-cloudcannon'); + module.exports = function (eleventyConfig) { const config = { pathPrefix: '/', @@ -35,159 +52,176 @@ module.exports = function (eleventyConfig) { data: '_my-custom-data', layouts: '_layouts', includes: '_my-includes' - }, - markdownItOptions: { - html: true } }; - // Either plugin looks here for plugin options - eleventyConfig.cloudcannonOptions = config; - - // Or plugin looks at what you return for plugin options + eleventyConfig.addPlugin(pluginCloudCannon, config); return config; }; ``` -If you set your custom input path with the `--input` CLI option, CloudCannon reads it automatically. - -## Plugin options - -The options are either set on the `eleventyConfig.cloudcannonOptions` key within your -`.eleventy.js` file, or passed to the `addPlugin` call as a second argument if you are adding the -plugin manually. +
+
-Should match the return value from your `.eleventy.js` (https://www.11ty.dev/docs/config/) file. +## Configuration -
- pathPrefix +This plugin uses a your `cloudcannon.config.*` configuration file as a base to generate +`_cloudcannon/info.json`. -> The `pathPrefix` setting your site uses. Defaults to: `'/'` +Configuration files must be in the same directory you run `npx @11ty/eleventy`. The first file +found is used, the files supported are: -
+- `cloudcannon.config.json` +- `cloudcannon.config.yaml` +- `cloudcannon.config.yml` +- `cloudcannon.config.js` +- `cloudcannon.config.cjs` -
- dir +Example content for `cloudcannon.config.cjs`: -> Custom paths your site uses. Defaults to: -> -> ```javascript -> { -> input: '.', -> data: '_data', -> includes: '_includes', -> layouts: '_includes' -> } -> ``` +```javascript +module.exports = { + // Global CloudCannon configuration + _inputs: { + title: { + type: 'text', + comment: 'The title of your page.' + } + }, + _select_data: { + colors: ['Red', 'Green', 'Blue'] + }, -
+ // Base path to your site source files, same as input for Eleventy + source: 'src', -
- markdownItOptions + // The subpath your built output files are mounted at + base_url: '/documentation', -> Options passed to markdown-it. Defaults to `{ html: true }`. + // Populates collections for navigation and metadata in the editor + collections_config: { + people: { + // Base path for files in this collection, relative to source + path: 'content/people', -
+ // Whether this collection produces output files or not + output: true -## Site configuration - -This plugin reads data from `cloudcannon` if available (i.e. `_data/cloudcannon.json` or -`_data/cloudcannon.js`). - -Details on each property here are listed in the relevant parts of the -[CloudCannon documentation](https://cloudcannon.com/documentation/). - -The following is an empty template as an example. - -```json -{ - "timezone": "", - "collections": { - "projects": { - "path": "", - "name": "", - "title": "", - "output": false, - "_sort_key": "", - "_subtext_key": "", - "_image_key": "", - "_image_size": "", - "_singular_name": "", - "_singular_key": "", - "_disable_add": false, - "_icon": "", - "_enabled_editors": null, - "_add_options": [] + // Collection-level configuration + name: 'Personnel', + _enabled_editors: ['data'] + }, + posts: { + path: '_posts', + output: true + }, + pages: { + name: 'Main pages' } }, - "_comments": {}, - "_options": {}, - "_editor": {}, - "_collection_groups": null, - "_source_editor": {}, - "_array_structures": {}, - "_enabled_editors": null, - "_select_data": {} -} + + // Generates the data for select and multiselect inputs matching these names + data_config: { + // Populates data with authors from an data file with the matching name + authors: true, + offices: true + }, + + paths: { + // The default location for newly uploaded files, relative to source + uploads: 'assets/uploads', + + // The path to site data files, relative to source + data: 'data', // defaults to _data + + // The path to site layout files, relative to source + layouts: '_layouts', // defaults to _includes + + // The path to site include files, relative to source + includes: '_partials' // defaults to _includes + } +}; ``` -## Manual installation +See the [CloudCannon documentation](https://cloudcannon.com/documentation/) for more information +on the available features you can configure. -Manually managing this plugin means you'll have to upgrade when new versions are released. You can -also follow these steps to try it or debug locally. +## Plugin options -
-Manual installation steps +Configuration is set in `cloudcannon.config.*`, but the plugin also automatically +reads the following from Eleventy if unset: -
+- `paths` from `dir` in `.eleventy.js` options +- `base_url` from `pathPrefix` in `.eleventy.js` options +- `source` from the `--input` CLI option or `dir.input` in `.eleventy.js` options -Start by enabling the "Manage eleventy-plugin-cloudcannon plugin manually" option in CloudCannon -for your site in *Site Settings / Build*. +These options match Eleventy's [configuration format](https://www.11ty.dev/docs/config/) and are +set in one of three ways: -```bash -npm install eleventy-plugin-cloudcannon --save -``` +
+ Returning from .eleventy.js with automatic installation -Add the following `addPlugin` call to the `module.exports` function in your `.eleventy.js` file: +**Requires automatic installation**. -```javascript -const pluginCloudCannon = require('eleventy-plugin-cloudcannon'); +> ```javascript +> module.exports = function (eleventyConfig) { +> return { +> pathPrefix: '/', +> dir: { +> input: '.', +> data: '_my-custom-data', +> layouts: '_layouts', +> includes: '_my-includes' +> } +> }; +> }; +> ``` -module.exports = function (eleventyConfig) { - eleventyConfig.addPlugin(pluginCloudCannon); -}; -``` +
-If you use custom paths for your site, pass them to the plugin as well: +
+ Setting eleventyConfig.cloudcannonOptions -```javascript -const pluginCloudCannon = require('eleventy-plugin-cloudcannon'); +**Requires automatic installation or needs to be before the call to `addPlugin`**. -module.exports = function (eleventyConfig) { - const config = { - pathPrefix: '/', - dir: { - input: '.', - data: '_my-custom-data', - layouts: '_layouts', - includes: '_my-includes' - } - }; +> ```javascript +> module.exports = function (eleventyConfig) { +> eleventyConfig.cloudcannonOptions = { +> pathPrefix: '/', +> dir: { +> input: '.', +> data: '_my-custom-data', +> layouts: '_layouts', +> includes: '_my-includes' +> } +> }; +> }; +> ``` - eleventyConfig.addPlugin(pluginCloudCannon, config); - return config; -}; -``` +
-Disabling the automatic injection prevents the `info.json` template being copied. In order to -retain this, add the following to your `.cloudcannon/prebuild` build hook: +
+ Through addPlugin with manual installation -```bash -rm -rf cloudcannon -cp -R node_modules/eleventy-plugin-cloudcannon/cloudcannon . -``` +> ```javascript +> const pluginCloudCannon = require('eleventy-plugin-cloudcannon'); +> +> module.exports = function (eleventyConfig) { +> const config = { +> pathPrefix: '/', +> dir: { +> input: '.', +> data: '_my-custom-data', +> layouts: '_layouts', +> includes: '_my-includes' +> } +> }; +> +> eleventyConfig.addPlugin(pluginCloudCannon, config); +> return config; +> }; +> ``` -
## License diff --git a/integration-tests/1.0.0-beta.8/site/_data/cloudcannon.json b/integration-tests/1.0.0-beta.8/site/_data/cloudcannon.json deleted file mode 100644 index ff107e6..0000000 --- a/integration-tests/1.0.0-beta.8/site/_data/cloudcannon.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "timezone": "Etc/UTC", - "_inputs": { - "layout": { - "hidden": "true" - } - }, - "collections": { - "authors": { - "name": "The Authors", - "path": "authors", - "output": false - } - } -} \ No newline at end of file diff --git a/integration-tests/1.0.0-beta.8/site/cloudcannon.config.js b/integration-tests/1.0.0-beta.8/site/cloudcannon.config.js new file mode 100644 index 0000000..0767e56 --- /dev/null +++ b/integration-tests/1.0.0-beta.8/site/cloudcannon.config.js @@ -0,0 +1,15 @@ +module.exports = { + timezone: 'Etc/UTC', + _inputs: { + layout: { + hidden: 'true' + } + }, + collections_config: { + authors: { + name: 'The Authors', + path: 'authors', + output: false + } + } +}; diff --git a/src/config.js b/src/config.js index 58d90b0..7e9713e 100644 --- a/src/config.js +++ b/src/config.js @@ -1,7 +1,7 @@ const { relative } = require('path'); const { cosmiconfigSync } = require('cosmiconfig'); -const { red, bold } = require('chalk'); -const { log } = require('./util/logger.js'); +const { red, bold, yellow } = require('chalk'); +const { log, logError } = require('./util/logger.js'); function readFileSync(configPath) { const moduleName = 'cloudcannon'; @@ -27,15 +27,15 @@ function readFileSync(configPath) { } } catch (e) { if (e.code === 'ENOENT') { - log(`⚠️ ${red('No config file found at')} ${red.bold(configPath)}`); + log(red(`⚠️ No config file found at ${bold(configPath)}`)); return false; - } else { - log(`⚠️ ${red('Error reading config file')}`, 'error'); - throw e; } + + logError(red('⚠️ Error reading config file')); + throw e; } - log('⚙️ No config file found'); + log(`⚙️ No config file found at ${bold('cloudcannon.config.*')}`); return false; } @@ -47,6 +47,10 @@ function rewriteKey(object, oldKey, newKey) { } function getLegacyConfig(context) { + if (context.cloudcannon) { + log(yellow(`⚙️ Falling back to ${bold('cloudcannon')} site data for config`)); + } + const legacy = context.cloudcannon || {}; rewriteKey(legacy, 'data', 'data_config'); @@ -74,8 +78,8 @@ function getLegacyConfig(context) { } function readConfig(context, options = {}) { + const file = readFileSync() || {}; // TODO custom config path here const legacy = getLegacyConfig(context); - const file = readFileSync() || {}; const baseUrl = file.base_url || options.pathPrefix || ''; @@ -95,6 +99,16 @@ function readConfig(context, options = {}) { } }; + if (options.markdownItOptions) { + config.generator = { + ...config.generator, + metadata: { + markdown: 'markdown-it', + 'markdown-it': options.markdownItOptions + } + }; + } + rewriteKey(config, '_source_editor', 'source_editor'); rewriteKey(config, '_editor', 'editor'); rewriteKey(config, '_collection_groups', 'collection_groups'); diff --git a/src/generators/generator.js b/src/generators/generator.js index 4ed0087..ad70458 100644 --- a/src/generators/generator.js +++ b/src/generators/generator.js @@ -1,4 +1,4 @@ -function getGenerator(context, options) { +function getGenerator(context, config) { const eleventyVersion = context.pkg?.dependencies?.['@11ty/eleventy'] || context.pkg?.devDependencies?.['@11ty/eleventy'] || ''; @@ -7,10 +7,12 @@ function getGenerator(context, options) { name: 'eleventy', version: eleventyVersion, environment: process.env.ELEVENTY_ENV || '', + ...config.generator, metadata: { markdown: 'markdown-it', - 'markdown-it': options?.markdownItOptions || { html: true } - } + 'markdown-it': { html: true }, + ...config.generator?.metadata + }, }; } diff --git a/src/generators/info.js b/src/generators/info.js index 8a31eb2..46e9940 100644 --- a/src/generators/info.js +++ b/src/generators/info.js @@ -8,7 +8,7 @@ const cloudcannon = { version: module.exports.version }; -function getInfo(context, config, options) { +function getInfo(context, config) { const collectionsConfig = getCollectionsConfig(context, config); const collections = getCollections(collectionsConfig, context, config); @@ -18,7 +18,7 @@ function getInfo(context, config, options) { collections: collections, collections_config: collectionsConfig, data: getData(context, config), - generator: getGenerator(context, options), + generator: getGenerator(context, config), time: new Date().toISOString(), version: '0.0.3' // schema version }; diff --git a/src/util/logger.js b/src/util/logger.js index 0577d89..3973a19 100644 --- a/src/util/logger.js +++ b/src/util/logger.js @@ -2,6 +2,11 @@ function log(str) { console.log(`[cloudcannon] ${str}`); } +function logError(str) { + console.error(`[cloudcannon] ${str}`); +} + module.exports = { - log + log, + logError }; diff --git a/tests/config.test.js b/tests/config.test.js index bd3b3c6..754cf3f 100644 --- a/tests/config.test.js +++ b/tests/config.test.js @@ -2,6 +2,13 @@ const test = require('ava'); const { readConfig } = require('../src/config.js'); test('reads legacy config', (t) => { + const options = { + markdownItOptions: { + html: true, + linkify: true + } + }; + const context = { cloudcannon: { data: { @@ -109,9 +116,18 @@ test('reads legacy config', (t) => { source_editor: { theme: 'dawn', }, + generator: { + metadata: { + markdown: 'markdown-it', + 'markdown-it': { + html: true, + linkify: true + } + } + } }; - const config = readConfig(context); + const config = readConfig(context, options); t.deepEqual(config, expected); }); diff --git a/tests/generators/generator.test.js b/tests/generators/generator.test.js index 42347b8..0e9bd55 100644 --- a/tests/generators/generator.test.js +++ b/tests/generators/generator.test.js @@ -1,15 +1,15 @@ const test = require('ava'); const { getGenerator } = require('../../src/generators/generator.js'); -const options = { - dir: { - input: '.', - data: '_data', - layouts: '_includes' - }, - markdownItOptions: { - html: true, - linkify: true +const config = { + generator: { + metadata: { + markdown: 'markdown-it', + 'markdown-it': { + html: true, + linkify: true + } + } } }; @@ -31,18 +31,18 @@ test('gets generator', (t) => { pkg: { dependencies: { '@11ty/eleventy': '1' } } }; - t.deepEqual(getGenerator(context, options), processedGenerator); + t.deepEqual(getGenerator(context, config), processedGenerator); const contextDev = { pkg: { devDependencies: { '@11ty/eleventy': '2' } } }; - t.deepEqual(getGenerator(contextDev, options), { + t.deepEqual(getGenerator(contextDev, config), { ...processedGenerator, version: '2' }); - t.deepEqual(getGenerator({}, options), { + t.deepEqual(getGenerator({}, config), { ...processedGenerator, version: '' }); diff --git a/tests/generators/info.test.js b/tests/generators/info.test.js index 39515c2..de83bfe 100644 --- a/tests/generators/info.test.js +++ b/tests/generators/info.test.js @@ -4,19 +4,16 @@ require('pkginfo')(module, 'version'); const version = module.exports.version || ''; -const options = { - dir: { - input: '.', - data: '_data', - layouts: '_includes' - }, - markdownItOptions: { - html: true, - linkify: true - } -}; - const config = { + generator: { + metadata: { + markdown: 'markdown-it', + 'markdown-it': { + html: true, + linkify: true + } + } + }, _select_data: { shrutes: ['Dwight', 'Mose'] }, @@ -160,7 +157,7 @@ test('gets info', (t) => { } }; - const result = getInfo(context, config, options); + const result = getInfo(context, config); t.deepEqual({ ...result, time: null }, processedInfo); const time = result.time.substring(0, 10);