Skip to content

Commit

Permalink
Merge pull request #7 from CloudCannon/build-commands-and-jekyll-impr…
Browse files Browse the repository at this point in the history
…ovements

Jekyll config improvements and build command start
  • Loading branch information
bglw committed Aug 27, 2024
2 parents 77c3b1e + 20f54c0 commit 8fd405d
Show file tree
Hide file tree
Showing 18 changed files with 735 additions and 89 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ package-lock.json
CHANGELOG.md
.github/
.backstage/
toolproof_tests/test_sites/
29 changes: 27 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function filterPaths(filePaths, source) {
* @param options {import('./types').GenerateOptions=} Options to aid generation.
* @returns {Promise<import('./types').GenerateResult>}
*/
export async function generate(filePaths, options) {
export async function generateConfiguration(filePaths, options) {
const ssg = options?.buildConfig?.ssg
? ssgs[options.buildConfig.ssg]
: guessSsg(filterPaths(filePaths, options?.config?.source));
Expand All @@ -50,10 +50,35 @@ export async function generate(filePaths, options) {
source,
collections_config:
options?.config?.collections_config ||
ssg.generateCollectionsConfig(collectionPaths, source),
ssg.generateCollectionsConfig(collectionPaths, { source, config }),
paths: options?.config?.paths ?? undefined,
timezone: options?.config?.timezone ?? ssg.getTimezone(),
markdown: options?.config?.markdown ?? ssg.generateMarkdown(config),
},
};
}

/**
* Generates a baseline CloudCannon configuration based on the file path provided.
*
* @param filePaths {string[]} List of input file paths.
* @param options {import('./types').GenerateOptions=} Options to aid generation.
* @returns {Promise<import('./types').BuildCommands>}
*/
export async function generateBuildCommands(filePaths, options) {
const ssg = options?.buildConfig?.ssg
? ssgs[options.buildConfig.ssg]
: guessSsg(filterPaths(filePaths, options?.config?.source));

const source = options?.config?.source ?? ssg.getSource(filePaths);
filePaths = filterPaths(filePaths, source);

const files = ssg.groupFiles(filePaths);

const configFilePaths = files.groups.config.map((fileSummary) => fileSummary.filePath);
const config = options?.readFile
? await ssg.parseConfig(configFilePaths, options.readFile)
: undefined;

return ssg.generateBuildCommands(filePaths, { config, source, readFile: options?.readFile });
}
22 changes: 22 additions & 0 deletions src/ssgs/eleventy.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,26 @@ export default class Eleventy extends Ssg {
},
};
}

/**
* Generates a list of build suggestions.
*
* @param filePaths {string[]} List of input file paths.
* @param options {{ config?: Record<string, any>; source?: string; readFile?: (path: string) => Promise<string | undefined>; }}
* @returns {Promise<import('../types').BuildCommands>}
*/
async generateBuildCommands(filePaths, options) {
const commands = await super.generateBuildCommands(filePaths, options);

commands.build.unshift({
value: 'npx @11ty/eleventy',
attribution: 'most common for 11ty sites',
});
commands.output.unshift({
value: '_site',
attribution: 'most common for 11ty sites',
});

return commands;
}
}
30 changes: 26 additions & 4 deletions src/ssgs/hugo.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ export default class Hugo extends Ssg {
*
* @param key {string}
* @param path {string}
* @param basePath {string}
* @param options {{ basePath: string; }=}
* @returns {import('@cloudcannon/configuration-types').CollectionConfig}
*/
generateCollectionConfig(key, path, basePath) {
const collectionConfig = super.generateCollectionConfig(key, path, basePath);
generateCollectionConfig(key, path, options) {
const collectionConfig = super.generateCollectionConfig(key, path, options);

if (path !== basePath) {
if (path !== options?.basePath) {
collectionConfig.glob =
typeof collectionConfig.glob === 'string'
? [collectionConfig.glob]
Expand Down Expand Up @@ -92,4 +92,26 @@ export default class Hugo extends Ssg {
options,
};
}

/**
* Generates a list of build suggestions.
*
* @param filePaths {string[]} List of input file paths.
* @param options {{ config?: Record<string, any>; source?: string; readFile?: (path: string) => Promise<string | undefined>; }}
* @returns {Promise<import('../types').BuildCommands>}
*/
async generateBuildCommands(filePaths, options) {
const commands = await super.generateBuildCommands(filePaths, options);

commands.build.unshift({
value: 'hugo',
attribution: 'most common for Hugo sites',
});
commands.output.unshift({
value: 'public',
attribution: 'most common for Hugo sites',
});

return commands;
}
}
183 changes: 160 additions & 23 deletions src/ssgs/jekyll.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { decodeEntity } from '../utility.js';
import { decodeEntity, joinPaths, stripTopPath } from '../utility.js';
import Ssg from './ssg.js';

/**
Expand All @@ -21,6 +21,85 @@ function isPostsPath(path) {
return !!path?.match(/\b_posts$/);
}

/**
* Transforms a Jekyll drafts collection path into a posts path.
*
* @param path {string} The drafts path.
* @returns {string}
*/
function toDraftsPath(path) {
return path.replace(/\b_posts$/, '_drafts');
}

/**
* Transforms a Jekyll posts collection path into a drafts path.
*
* @param path {string} The posts path.
* @returns {string}
*/
function toPostsPath(path) {
return path.replace(/\b_drafts$/, '_posts');
}

/**
* Transforms a drafts collection key into a posts key.
*
* @param key {string} The drafts key.
* @returns {string}
*/
function toDraftsKey(key) {
return key.replace('posts', 'drafts');
}

/**
* Transforms a posts collection key into a drafts key.
*
* @param key {string} The posts key.
* @returns {string}
*/
function toPostsKey(key) {
return key.replace('drafts', 'posts');
}

/**
* Gets `collections` from Jekyll configuration in a standard format.
*
* @param collections {Record<string, any> | undefined} The `collections` object from Jekyll config
* @returns {Record<string, any>}
*/
function getJekyllCollections(collections) {
/** @type {Record<string, any>} */
let formatted = {};

if (Array.isArray(collections)) {
return collections.reduce((memo, key) => {
memo[key] = {};
return memo;
}, formatted);
}

if (typeof collections === 'object') {
return collections;
}

return formatted;
}

/**
* Checks if a Jekyll collection is output.
*
* @param key {string}
* @param collection {Record<string, any> | undefined}
* @returns {boolean}
*/
function isCollectionOutput(key, collection) {
if (key === 'data' || key === 'drafts' || key.endsWith('_drafts')) {
return false;
}

return key === 'pages' || key === 'posts' || key.endsWith('_posts') || !!collection?.output;
}

export default class Jekyll extends Ssg {
constructor() {
super('jekyll');
Expand Down Expand Up @@ -99,33 +178,33 @@ export default class Jekyll extends Ssg {
*
* @param key {string}
* @param path {string}
* @param options {{ basePath?: string; collection: Record<string, any> | undefined; }}
* @returns {import('@cloudcannon/configuration-types').CollectionConfig}
*/
generateCollectionConfig(key, path) {
generateCollectionConfig(key, path, options) {
const collectionConfig = super.generateCollectionConfig(key, path);
// TODO: read contents of _config.yml to find which collections are output
collectionConfig.output = key !== 'data';

collectionConfig.output = isCollectionOutput(key, options.collection);

if (options.collection?.sort_by) {
collectionConfig.sort = { key: options.collection.sort_by };
}

if (isPostsPath(collectionConfig.path)) {
collectionConfig.create ||= {
path: '[relative_base_path]/{date|year}-{date|month}-{date|day}-{title|slugify}.[ext]',
};

collectionConfig.add_options ||= [
{
name: `Add ${collectionConfig.singular_name || 'Post'}`,
},
{
name: 'Add Draft',
collection: key.replace('posts', 'drafts'),
},
{ name: `Add ${collectionConfig.singular_name || 'Post'}` },
{ name: 'Add Draft', collection: toDraftsKey(key) },
];
}

if (isDraftsPath(collectionConfig.path)) {
collectionConfig.create ||= {
path: '', // TODO: this should not be required if publish_to is set
publish_to: key.replace('drafts', 'posts'),
publish_to: toPostsKey(key),
};
}

Expand All @@ -136,30 +215,54 @@ export default class Jekyll extends Ssg {
* Generates collections config from a set of paths.
*
* @param collectionPaths {{ basePath: string, paths: string[] }}
* @param source {string | undefined}
* @param options {{ config?: Record<string, any>; source?: string; }=}
* @returns {import('../types').CollectionsConfig}
*/
generateCollectionsConfig(collectionPaths, source) {
const collectionsConfig = super.generateCollectionsConfig(collectionPaths, source);
generateCollectionsConfig(collectionPaths, options) {
/** @type {import('../types').CollectionsConfig} */
const collectionsConfig = {};
const collectionsDir = options?.config?.collections_dir || '';
const collections = getJekyllCollections(options?.config?.collections);

// Content folder to collections_config mapping.
for (const fullPath of collectionPaths.paths) {
const path = stripTopPath(fullPath, options?.source);
const pathInCollectionsDir = collectionsDir ? stripTopPath(path, collectionsDir) : path;
const key = this.generateCollectionsConfigKey(pathInCollectionsDir, collectionsConfig);
const collection = collections[stripTopPath(path, collectionsDir).replace(/^\/?_/, '')];
collectionsConfig[key] = this.generateCollectionConfig(key, path, { collection });
}

const keys = Object.keys(collectionsConfig);
// Handle defined collections without files.
for (const key of Object.keys(collections)) {
collectionsConfig[key] ||= {
path: joinPaths([collectionsDir, `_${key}`]),
output: isCollectionOutput(key, collections[key]),
};
}

for (const key of keys) {
// Add matching post/draft collections
for (const key of Object.keys(collectionsConfig)) {
const collectionConfig = collectionsConfig[key];
if (!collectionConfig.path) {
continue;
}

if (isDraftsPath(collectionConfig.path) && collectionConfig.path) {
if (isDraftsPath(collectionConfig.path)) {
// Ensure there is a matching posts collection
const postsKey = key.replace('drafts', 'posts');
const postsKey = toPostsKey(key);
collectionsConfig[postsKey] ||= this.generateCollectionConfig(
postsKey,
collectionConfig.path?.replace(/\b_drafts$/, '_posts'),
toPostsPath(collectionConfig.path),
{ collection: collections?.posts },
);
} else if (isPostsPath(collectionConfig.path) && collectionConfig.path) {
} else if (isPostsPath(collectionConfig.path)) {
// Ensure there is a matching drafts collection
const draftsKey = key.replace('posts', 'drafts');
const draftsKey = toDraftsKey(key);
collectionsConfig[draftsKey] ||= this.generateCollectionConfig(
draftsKey,
collectionConfig.path?.replace(/\b_posts$/, '_drafts'),
toDraftsPath(collectionConfig.path),
{ collection: collections?.drafts || collections?.posts },
);
}
}
Expand Down Expand Up @@ -233,4 +336,38 @@ export default class Jekyll extends Ssg {
options,
};
}

/**
* Generates a list of build suggestions.
*
* @param filePaths {string[]} List of input file paths.
* @param options {{ config?: Record<string, any>; source?: string; readFile?: (path: string) => Promise<string | undefined>; }}
* @returns {Promise<import('../types').BuildCommands>}
*/
async generateBuildCommands(filePaths, options) {
const commands = await super.generateBuildCommands(filePaths, options);

if (filePaths.includes(joinPaths([options.source, 'Gemfile']))) {
commands.install.unshift({
value: 'bundle install',
attribution: 'because of your Gemfile',
});
commands.build.unshift({
value: 'bundle exec jekyll build',
attribution: 'because of your Gemfile',
});
} else {
commands.build.unshift({
value: 'jekyll build',
attribution: 'most common for Jekyll sites',
});
}

commands.output.unshift({
value: '_site',
attribution: 'most common for Jekyll sites',
});

return commands;
}
}
Loading

0 comments on commit 8fd405d

Please sign in to comment.