Skip to content

Commit

Permalink
Merge pull request #13 from CloudCannon/hugo/collections
Browse files Browse the repository at this point in the history
Better detection for Hugo sites
  • Loading branch information
georgephillips authored Aug 29, 2024
2 parents 4de7269 + 8456878 commit e6e1aba
Show file tree
Hide file tree
Showing 19 changed files with 527 additions and 182 deletions.
33 changes: 8 additions & 25 deletions src/collections.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { join } from 'path';
import { stripTopPath } from './utility.js';

/**
* Produces an ordered set of paths that a file at this path could belong to.
Expand All @@ -12,7 +11,7 @@ export function getCollectionPaths(filePath) {
const paths = [''];
const parts = filePath.split('/');

for (var i = 0; i < parts.length - 1; i++) {
for (let i = 0; i < parts.length - 1; i++) {
builder = join(builder, parts[i]);
paths.push(builder);
}
Expand All @@ -21,19 +20,18 @@ export function getCollectionPaths(filePath) {
}

/**
* Makes collection paths relative to a shared base path, also returns that shared path as source.
* Finds a shared base path.
*
* @param collectionPathCounts {Record<string, number>}
* @returns {{ basePath: string, paths: string[] }}
* @param paths {string[]}
* @returns {basePath}
*/
export function processCollectionPaths(collectionPathCounts) {
let paths = Object.keys(collectionPathCounts);
export function findBasePath(paths) {
let basePath = '';

if (paths.length) {
if (paths.length > 1) {
const checkParts = paths[0].split('/');

for (var i = 0; i < checkParts.length; i++) {
for (let i = 0; i < checkParts.length; i++) {
const checkPath = join(...checkParts.slice(0, i + 1));

const isSharedPath =
Expand All @@ -46,20 +44,5 @@ export function processCollectionPaths(collectionPathCounts) {
}
}

if (basePath) {
paths = paths.map((pathKey) => stripTopPath(pathKey, basePath));
}

if (paths.length === 1) {
// If there is one collection, force it to have path.
return {
basePath: '',
paths: [basePath],
};
}

return {
basePath,
paths,
};
return basePath;
}
1 change: 1 addition & 0 deletions src/icons.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function findIcon(query) {
const overrides = {
collection_pages: 'web_asset',
pages: 'wysiwyg',
content: 'wysiwyg',
posts: 'event_available',
post: 'event_available',
blog: 'event_available',
Expand Down
10 changes: 7 additions & 3 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { guessSsg, ssgs } from './ssgs/ssgs.js';
import { processCollectionPaths } from './collections.js';
import { findBasePath } from './collections.js';

export { ssgs } from './ssgs/ssgs.js';

Expand Down Expand Up @@ -42,15 +42,19 @@ export async function generateConfiguration(filePaths, options) {
? await ssg.parseConfig(configFilePaths, options.readFile)
: undefined;

const collectionPaths = processCollectionPaths(files.collectionPathCounts);
const collectionPaths = Object.keys(files.collectionPathCounts);

return {
ssg: ssg.key,
config: {
source,
collections_config:
options?.config?.collections_config ||
ssg.generateCollectionsConfig(collectionPaths, { source, config }),
ssg.generateCollectionsConfig(collectionPaths, {
source,
config,
basePath: findBasePath(collectionPaths),
}),
paths: options?.config?.paths ?? undefined,
timezone: options?.config?.timezone ?? ssg.getTimezone(),
markdown: options?.config?.markdown ?? ssg.generateMarkdown(config),
Expand Down
129 changes: 116 additions & 13 deletions src/ssgs/hugo.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { decodeEntity } from '../utility.js';
import { findBasePath } from '../collections.js';
import { decodeEntity, joinPaths, stripTopPath } from '../utility.js';
import Ssg from './ssg.js';

export default class Hugo extends Ssg {
Expand All @@ -7,9 +8,17 @@ export default class Hugo extends Ssg {
}

configPaths() {
return super
.configPaths()
.concat(['hugo.toml', 'hugo.yaml', 'hugo.json', 'config.toml', 'config.yaml', 'config.json']);
return super.configPaths().concat([
'hugo.toml',
'hugo.yml',
'hugo.yaml',
'hugo.json',
'theme.toml', // on theme repos
'config.toml',
'config.yml',
'config.yaml',
'config.json',
]);
}

templateExtensions() {
Expand All @@ -22,34 +31,125 @@ export default class Hugo extends Ssg {
]);
}

ignoredFiles() {
return super.ignoredFiles().concat([
'theme.toml', // config file for a hugo theme repo
]);
}

ignoredFolders() {
return super.ignoredFolders().concat([
'static/', // passthrough asset folder
'assets/', // unprocessed asset folder
'public/', // default output
'resources/', // cache
]);
}

// These are customisable, but likely fine to use for source detection regardless.
conventionalPathsInSource = [
'archetypes/',
'assets/',
'content/',
'config/',
'data/',
'i18n/',
'layouts/',
'static/',
'themes/',
];

/**
* Generates a collection config entry.
*
* @param key {string}
* @param path {string}
* @param options {{ basePath: string; }=}
* @param options {{ basePath: string; }}
* @returns {import('@cloudcannon/configuration-types').CollectionConfig}
*/
generateCollectionConfig(key, path, options) {
const collectionConfig = super.generateCollectionConfig(key, path, options);

if (path !== options?.basePath) {
if (path !== options.basePath) {
collectionConfig.glob =
typeof collectionConfig.glob === 'string'
? [collectionConfig.glob]
: collectionConfig.glob || [];
collectionConfig.glob.push('!_index.md');
}

collectionConfig.output = !(path === 'data' || path.endsWith('/data'));

return collectionConfig;
}

/**
* Generates collections config from a set of paths.
*
* @param collectionPaths {string[]}
* @param options {{ config?: Record<string, any>; source?: string; basePath: string; }}
* @returns {import('../types').CollectionsConfig}
*/
generateCollectionsConfig(collectionPaths, options) {
/** @type {import('../types').CollectionsConfig} */
const collectionsConfig = {};
let basePath = options.basePath;

const collectionPathsOutsideExampleSite = collectionPaths.filter(
(path) => !path.includes('exampleSite/'),
);

const hasNonExampleSiteCollections =
collectionPathsOutsideExampleSite.length &&
collectionPathsOutsideExampleSite.length !== collectionPaths.length;

// Exclude collections found inside the exampleSite folder, unless they are the only collections
if (hasNonExampleSiteCollections) {
basePath = findBasePath(collectionPathsOutsideExampleSite);
collectionPaths = collectionPathsOutsideExampleSite;
}

const dataPath = joinPaths([basePath, 'data']);
const collectionPathsOutsideData = collectionPaths.filter((path) => !path.startsWith(dataPath));
const hasDataCollection =
collectionPathsOutsideData.length &&
collectionPathsOutsideData.length !== collectionPaths.length;

// Reprocess basePath to exclude the data folder
if (hasDataCollection) {
basePath = findBasePath(collectionPathsOutsideData);
}

basePath = stripTopPath(basePath, options.source);

const sortedPaths = collectionPaths.sort((a, b) => a.length - b.length);
/** @type {string[]} */
const seenPaths = [];

for (const fullPath of sortedPaths) {
const path = stripTopPath(fullPath, options.source);
const pathInBasePath = stripTopPath(path, basePath);

if (
!path.startsWith(dataPath) &&
seenPaths.some((seenPath) => pathInBasePath.startsWith(seenPath))
) {
// Skip collection if not data, or a top-level content collection (i.e. seen before)
continue;
}

if (pathInBasePath) {
seenPaths.push(pathInBasePath + '/');
}

const key = this.generateCollectionsConfigKey(pathInBasePath, collectionsConfig);

collectionsConfig[key] = this.generateCollectionConfig(key, path, { basePath });
}

return collectionsConfig;
}

/**
* @param config {Record<string, any> | undefined}
* @returns {import('@cloudcannon/configuration-types').MarkdownSettings}
Expand Down Expand Up @@ -139,13 +239,16 @@ export default class Hugo extends Ssg {
attribution: 'recommended for Hugo sites',
};

commands.preserved.push({
value: 'resources/',
attribution: 'recommended for speeding up Hugo builds',
}, {
value: '.hugo_cache/',
attribution: 'recommended for speeding up Hugo builds',
});
commands.preserved.push(
{
value: 'resources/',
attribution: 'recommended for speeding up Hugo builds',
},
{
value: '.hugo_cache/',
attribution: 'recommended for speeding up Hugo builds',
},
);

return commands;
}
Expand Down
58 changes: 9 additions & 49 deletions src/ssgs/jekyll.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,48 +132,7 @@ export default class Jekyll extends Ssg {
}

// _posts and _drafts excluded here as they are potentially nested deeper.
static conventionPaths = ['_plugins/', '_includes/', '_data/', '_layouts/', '_sass/'];

/**
* Attempts to find the most likely source folder for a Jekyll site.
*
* @param filePaths {string[]} List of input file paths.
* @returns {{ filePath?: string, conventionPath?: string }}
*/
_findConventionPath(filePaths) {
for (let i = 0; i < filePaths.length; i++) {
for (let j = 0; j < filePaths.length; j++) {
if (
filePaths[i].startsWith(Jekyll.conventionPaths[j]) ||
filePaths[i].includes(Jekyll.conventionPaths[j])
) {
return {
filePath: filePaths[i],
conventionPath: Jekyll.conventionPaths[j],
};
}
}
}

return {};
}

/**
* Attempts to find the most likely source folder for a Jekyll site.
*
* @param filePaths {string[]} List of input file paths.
* @returns {string | undefined}
*/
getSource(filePaths) {
const { filePath, conventionPath } = this._findConventionPath(filePaths);

if (filePath && conventionPath) {
const conventionIndex = filePath.indexOf(conventionPath);
return filePath.substring(0, Math.max(0, conventionIndex - 1));
}

return super.getSource(filePaths);
}
conventionalPathsInSource = ['_plugins/', '_includes/', '_data/', '_layouts/', '_sass/'];

/**
* Generates a collection config entry.
Expand Down Expand Up @@ -214,15 +173,15 @@ export default class Jekyll extends Ssg {
/**
* Generates collections config from a set of paths.
*
* @param collectionPaths {{ basePath: string, paths: string[] }}
* @param options {{ config?: Record<string, any>; source?: string; }=}
* @param collectionPaths {string[]}
* @param options {{ config?: Record<string, any>; source?: string; basePath: string; }}
* @returns {import('../types').CollectionsConfig}
*/
generateCollectionsConfig(collectionPaths, options) {
/** @type {import('../types').CollectionsConfig} */
const collectionsConfig = {};
const collectionsDir = options?.config?.collections_dir || '';
const collections = getJekyllCollections(options?.config?.collections);
const collectionsDir = options.config?.collections_dir || '';
const collections = getJekyllCollections(options.config?.collections);

// Handle defined collections.
for (const key of Object.keys(collections)) {
Expand All @@ -235,14 +194,15 @@ export default class Jekyll extends Ssg {
});
}

const sortedPaths = collectionPaths.paths.sort((a, b) => a.length - b.length);
const sortedPaths = collectionPaths.sort((a, b) => a.length - b.length);

// Use detected content folders to handle automatic/default collections.
for (const fullPath of sortedPaths) {
const path = stripTopPath(fullPath, options?.source);
const path = stripTopPath(fullPath, options.source);

const isDefaultCollection =
path === sortedPaths[0] || // root folder, or a subfolder if no content files in root
sortedPaths.length === 1 || // a subfolder if no content files in root
path === '' || // root folder
path === '_data' ||
path.startsWith('_data/') ||
path === '_posts' ||
Expand Down
Loading

0 comments on commit e6e1aba

Please sign in to comment.