From fb77135d782b8fb0f5b3b8f19f2d113f3d49ca74 Mon Sep 17 00:00:00 2001 From: Ross Phillips <12723297+rphillips-cc@users.noreply.github.com> Date: Wed, 12 Jun 2024 10:54:37 +1200 Subject: [PATCH] Split SSGs into separate files for overrides --- src/cli.js | 4 +- src/collections.js | 36 +++--- src/index.js | 60 +++++---- src/ssg.js | 266 --------------------------------------- src/ssgs/bridgetown.js | 15 +++ src/ssgs/eleventy.js | 30 +++++ src/ssgs/hugo.js | 27 ++++ src/ssgs/jekyll.js | 77 ++++++++++++ src/ssgs/next-js.js | 23 ++++ src/ssgs/ssg.js | 175 ++++++++++++++++++++++++++ src/ssgs/ssgs.js | 49 ++++++++ src/ssgs/sveltekit.js | 27 ++++ src/types.d.ts | 19 ++- src/utility.js | 15 +++ test/collections.test.js | 16 +++ test/ssgs/jekyll.test.js | 15 +++ test/utility.test.js | 8 +- 17 files changed, 552 insertions(+), 310 deletions(-) delete mode 100644 src/ssg.js create mode 100644 src/ssgs/bridgetown.js create mode 100644 src/ssgs/eleventy.js create mode 100644 src/ssgs/hugo.js create mode 100644 src/ssgs/jekyll.js create mode 100644 src/ssgs/next-js.js create mode 100644 src/ssgs/ssg.js create mode 100644 src/ssgs/ssgs.js create mode 100644 src/ssgs/sveltekit.js create mode 100644 test/collections.test.js create mode 100644 test/ssgs/jekyll.test.js diff --git a/src/cli.js b/src/cli.js index 79bb127..f8f600b 100755 --- a/src/cli.js +++ b/src/cli.js @@ -5,6 +5,7 @@ import meow from 'meow'; import { exit } from 'process'; +import { readFile } from 'fs/promises'; import { fdir } from 'fdir'; import { generate } from './index.js'; @@ -61,8 +62,7 @@ if (!folderPath) { } else { const crawler = new fdir().withRelativePaths().filter((filePath) => !isIgnoredPath(filePath)); const filePaths = await crawler.crawl(folderPath).withPromise(); - const config = await generate(filePaths); + const config = await generate(filePaths, { readFile }); - console.debug(filePaths); console.log(JSON.stringify(config, undefined, 2)); } diff --git a/src/collections.js b/src/collections.js index b8cadb1..c656ba6 100644 --- a/src/collections.js +++ b/src/collections.js @@ -1,7 +1,8 @@ -import { basename, join, sep } from 'path'; -import { findIcon } from './icons.js'; +import { basename, join } from 'path'; import slugify from '@sindresorhus/slugify'; import titleize from 'titleize'; +import { findIcon } from './icons.js'; +import { stripTopPath } from './utility.js'; /** * Produces an ordered set of paths that a file at this path could belong to. @@ -12,7 +13,7 @@ import titleize from 'titleize'; export function getCollectionPaths(filePath) { let builder = ''; const paths = ['']; - const parts = filePath.split(sep); + const parts = filePath.split('/'); for (var i = 0; i < parts.length - 1; i++) { builder = join(builder, parts[i]); @@ -26,28 +27,30 @@ export function getCollectionPaths(filePath) { * Generates collections config from a set of paths. * * @param collectionPaths {{ basePath: string, paths: string[] }} - * @returns {Record.} + * @param source {string} + * @returns {import('./types').CollectionsConfig} */ -export function generateCollectionsConfig(collectionPaths) { - /** @type Record */ +export function generateCollectionsConfig(collectionPaths, source) { + /** @type import('./types').CollectionsConfig */ const collectionsConfig = {}; + const collectionPath = stripTopPath(collectionPaths.basePath, source); for (let path of collectionPaths.paths) { - const key = slugify(path) || 'pages'; + const sourcePath = stripTopPath(path, source); + const key = slugify(sourcePath) || 'pages'; const name = titleize( - basename(path || key) + basename(sourcePath || key) .replace(/[_-]/g, ' ') .trim(), ); collectionsConfig[key] = { - path, + path: sourcePath, name, icon: findIcon(name.toLowerCase()), }; - console.log(path, collectionPaths.basePath); - if (path === collectionPaths.basePath) { + if (sourcePath === collectionPath) { collectionsConfig[key].filter = { base: 'strict', }; @@ -68,11 +71,14 @@ export function processCollectionPaths(collectionPathCounts) { let basePath = ''; if (paths.length) { - const checkParts = paths[0].split(sep); + const checkParts = paths[0].split('/'); for (var i = 0; i < checkParts.length; i++) { - const checkPath = join(...checkParts.slice(0, i)); - const isSharedPath = paths.every((pathKey) => pathKey.startsWith(checkPath + sep)); + const checkPath = join(...checkParts.slice(0, i + 1)); + + const isSharedPath = + checkPath && + paths.every((pathKey) => pathKey === checkPath || pathKey.startsWith(checkPath + '/')); if (isSharedPath) { basePath = checkPath; @@ -81,7 +87,7 @@ export function processCollectionPaths(collectionPathCounts) { } if (basePath) { - paths = paths.map((pathKey) => pathKey.substring(basePath.length + 1)); + paths = paths.map((pathKey) => stripTopPath(pathKey, basePath)); } return { diff --git a/src/index.js b/src/index.js index 18eb30a..ae4dcfa 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ -import { guessSsg } from './ssg.js'; -import { last } from './utility.js'; +import { guessSsg } from './ssgs/ssgs.js'; +import { last, stripTopPath } from './utility.js'; import { getCollectionPaths, generateCollectionsConfig, @@ -9,11 +9,11 @@ import { /** * Provides a summary of a file at this path. * - * @param filePath {string} - * @param ssg {import('./ssg.js').Ssg} - * @returns {import('./types.d.ts').FileSummary} + * @param filePath {string} The input file path. + * @param ssg {import('./ssgs/ssg').default} The associated SSG. + * @returns {import('./types').ParsedFile} Summary of the file. */ -function generateFile(filePath, ssg) { +function parseFile(filePath, ssg) { const type = ssg.getFileType(filePath); return { @@ -23,20 +23,19 @@ function generateFile(filePath, ssg) { } /** - * Generates a baseline CLoudCannon configuration based on the file path provided. + * Provides a summary of files. * - * @param filePaths {string[]} List of input file paths. - * @param _options {import('./types.d.ts').GenerateOptions=} Options to aid generation. - * @returns {Promise} + * @param filePaths {string[]} The input file path. + * @param ssg {import('./ssgs/ssg').default} The associated SSG. + * @param source {string} The site's source path. + * @returns {import('./types').ParsedFiles} The file summaries grouped by type. */ -export async function generate(filePaths, _options) { - const ssg = guessSsg(filePaths); - +function parseFiles(filePaths, ssg) { /** @type {Record} */ const collectionPathCounts = {}; - /** @type {Record} */ - const files = { + /** @type {Record} */ + const groups = { config: [], content: [], partial: [], @@ -46,7 +45,7 @@ export async function generate(filePaths, _options) { }; for (let i = 0; i < filePaths.length; i++) { - const file = generateFile(filePaths[i], ssg); + const file = parseFile(filePaths[i], ssg); if (file.type === 'content') { const lastPath = last(getCollectionPaths(filePaths[i])); @@ -57,18 +56,35 @@ export async function generate(filePaths, _options) { } if (file.type !== 'ignored') { - files[file.type].push(file); + groups[file.type].push(file); } } - const collectionPaths = processCollectionPaths(collectionPathCounts); - console.log('collectionPaths', collectionPaths); + return { collectionPathCounts, groups }; +} + +/** + * 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} + */ +export async function generate(filePaths, options) { + const ssg = guessSsg(filePaths); + const files = parseFiles(filePaths, ssg); + const collectionPaths = processCollectionPaths(files.collectionPathCounts); + const source = + options?.userConfig?.source ?? + options?.buildConfig?.source ?? + ssg.getSource(files, filePaths, collectionPaths); return { - source: '', - collections_config: generateCollectionsConfig(collectionPaths), + ssg: ssg?.key, + source, + collections_config: generateCollectionsConfig(collectionPaths, source), paths: { - collections: collectionPaths.basePath, + collections: stripTopPath(collectionPaths.basePath, source), }, }; } diff --git a/src/ssg.js b/src/ssg.js deleted file mode 100644 index 6ce6235..0000000 --- a/src/ssg.js +++ /dev/null @@ -1,266 +0,0 @@ -import { extname } from 'path'; - -export class Ssg { - /** @type {import('./types.d.ts').SsgKey} */ - key; - - /** @type {string[]} */ - configPaths; - - /** @type {string[]} */ - templateExtensions; - - /** @type {string[]} */ - contentExtensions; - - /** @type {string[]} */ - partialFolders; - - /** @type {string[]} */ - ignoredFolders; - - /** - * @param key {import('./types.d.ts').SsgKey} - * @param configPaths {{ configPaths: string[], templateExtensions: string[], contentExtensions: string[], partialFolders: string[], ignoredFolders: string[] }} - */ - constructor( - key, - { configPaths, templateExtensions, contentExtensions, partialFolders, ignoredFolders }, - ) { - this.key = key; - this.configPaths = configPaths || []; - this.templateExtensions = templateExtensions || []; - this.contentExtensions = contentExtensions || []; - this.partialFolders = partialFolders || []; - this.ignoredFolders = ignoredFolders || []; - } - - /** - * Checks if the file at this path is an SSG configuration file. - * - * @param filePath {string} - * @returns {boolean} - */ - isConfigPath(filePath) { - return this.configPaths.some( - (configPath) => filePath === configPath || filePath.endsWith(`/${configPath}`), - ); - } - - /** - * Returns a score for how likely a file path relates to this SSG. - * - * @param filePath {string} - * @returns {number} - */ - getPathScore(filePath) { - return this.isConfigPath(filePath) ? 1 : 0; - } - - /** - * Checks if we should skip a file at this path - * - * @param filePath {string} - * @returns {boolean} - */ - isIgnoredPath(filePath) { - return this.ignoredFolders.some((folder) => filePath.startsWith(folder)); - } - - /** - * Checks if the file at this path is a contains Markdown or structured content. - * - * @param filePath {string} - * @returns {boolean} - */ - isContentPath(filePath) { - return this.contentExtensions.includes(extname(filePath)); - } - - /** - * Checks if the file at this path is an include, partial or layout file. - * - * @param filePath {string} - * @returns {boolean} - */ - isPartialPath(filePath) { - return this.partialFolders.some( - (partialFolder) => filePath === partialFolder || filePath.includes(`/${partialFolder}`), - ); - } - - /** - * Checks if the file at this path is a template file. - * - * @param filePath {string} - * @returns boolean - */ - isTemplatePath(filePath) { - return this.templateExtensions.includes(extname(filePath)); - } - - /** - * Finds the likely type of the file at this path. - * - * @param filePath {string} - * @returns {import('./types.d.ts').FileType} - */ - getFileType(filePath) { - if (this.isIgnoredPath(filePath)) { - return 'ignored'; - } - - if (this.isPartialPath(filePath)) { - return 'partial'; - } - - if (this.isConfigPath(filePath)) { - return 'config'; - } - - if (this.isTemplatePath(filePath)) { - return 'template'; - } - - if (this.isContentPath(filePath)) { - return 'content'; - } - - return 'other'; - } -} - -const defaultContentExtensions = [ - '.md', - '.mdown', - '.markdown', - '.mdx', - '.json', - '.yml', - '.yaml', - '.toml', - '.csv', - '.tsv', -]; - -const defaultIncludeFolders = [ - 'layouts/', // general partials - 'components/', // general partials - 'component-library/', // general partials - 'schemas/', // CloudCannon schema files -]; - -const defaultTemplateExtensions = ['.htm', '.html']; - -const unknownSsg = new Ssg('unknown', { - configPaths: [], - templateExtensions: defaultTemplateExtensions, - contentExtensions: defaultContentExtensions, - partialFolders: defaultIncludeFolders, - ignoredFolders: [], -}); - -const ssgs = [ - new Ssg('hugo', { - configPaths: ['config.toml', 'hugo.toml', 'hugo.yaml', 'hugo.json'], - templateExtensions: [], - contentExtensions: defaultContentExtensions, - partialFolders: defaultIncludeFolders.concat(['archetypes/']), - ignoredFolders: [ - 'resources/', // cache - 'archetypes/', // scaffolding templates - ], - }), - - new Ssg('jekyll', { - configPaths: ['_config.yml', '_config.yaml'], - templateExtensions: defaultTemplateExtensions.concat(['.liquid']), - contentExtensions: defaultContentExtensions, - partialFolders: defaultIncludeFolders.concat(['_layouts/']), - ignoredFolders: [ - '_site/', // build output - '.jekyll-cache/', // cache - ], - }), - - new Ssg('bridgetown', { - configPaths: ['_config.yml', '_config.yaml'], - templateExtensions: defaultTemplateExtensions.concat(['.liquid']), - contentExtensions: defaultContentExtensions, - partialFolders: defaultIncludeFolders, - ignoredFolders: [], - }), - - new Ssg('eleventy', { - configPaths: ['eleventy.config.js', 'eleventy.config.cjs', '.eleventy.cjs'], - templateExtensions: defaultTemplateExtensions.concat([ - '.njk', - '.liquid', - '.hbs', - '.ejs', - '.webc', - '.mustache', - '.haml', - '.pug', - ]), - contentExtensions: defaultContentExtensions, - partialFolders: defaultIncludeFolders, - ignoredFolders: [], - }), - - new Ssg('sveltekit', { - configPaths: ['svelte.config.js'], - templateExtensions: defaultTemplateExtensions.concat(['.svelte']), - contentExtensions: defaultContentExtensions.concat(['.svx']), - partialFolders: defaultIncludeFolders, - ignoredFolders: [ - 'build/', // build output - '.svelte-kit/', // cache - 'static/', // static assets - ], - }), - - new Ssg('nextjs', { - configPaths: ['next.config.js', 'next.config.mjs'], - templateExtensions: defaultTemplateExtensions, - contentExtensions: defaultContentExtensions, - partialFolders: defaultIncludeFolders, - ignoredFolders: [ - 'out/', // build output - '.next/', // cache - 'public/', // static assets - ], - }), - - unknownSsg, -]; - -/** - * Finds the most likely SSG for a set of files. - * - * @param filePaths {string[]} - * @returns {Ssg} - */ -export function guessSsg(filePaths) { - /** @type {Record} */ - const scores = { - eleventy: 0, - jekyll: 0, - bridgetown: 0, - hugo: 0, - sveltekit: 0, - nextjs: 0, - unknown: 0, - }; - - for (let i = 0; i < filePaths.length; i++) { - for (let j = 0; j < ssgs.length; j++) { - scores[ssgs[j].key] += ssgs[j].getPathScore(filePaths[i]); - } - } - - return ssgs.reduce( - (previous, current) => (scores[previous.key] < scores[current.key] ? current : previous), - unknownSsg, - ); -} diff --git a/src/ssgs/bridgetown.js b/src/ssgs/bridgetown.js new file mode 100644 index 0000000..600596f --- /dev/null +++ b/src/ssgs/bridgetown.js @@ -0,0 +1,15 @@ +import Ssg from './ssg.js'; + +export default class Bridgetown extends Ssg { + constructor() { + super('bridgetown'); + } + + configPaths() { + return super.configPaths().concat(['_config.yml', '_config.yaml']); + } + + templateExtensions() { + return super.templateExtensions().concat(['.liquid']); + } +} diff --git a/src/ssgs/eleventy.js b/src/ssgs/eleventy.js new file mode 100644 index 0000000..1bc1ee9 --- /dev/null +++ b/src/ssgs/eleventy.js @@ -0,0 +1,30 @@ +import Ssg from './ssg.js'; + +export default class Eleventy extends Ssg { + constructor() { + super('eleventy'); + } + + configPaths() { + return super + .configPaths() + .concat([ + 'eleventy.config.js', + 'eleventy.config.mjs', + 'eleventy.config.cjs', + '.eleventy.mjs', + '.eleventy.cjs', + '.eleventy.js', + ]); + } + + templateExtensions() { + return super + .templateExtensions() + .concat(['.njk', '.liquid', '.hbs', '.ejs', '.webc', '.mustache', '.haml', '.pug']); + } + + contentExtensions() { + return super.contentExtensions().concat(['.html']); + } +} diff --git a/src/ssgs/hugo.js b/src/ssgs/hugo.js new file mode 100644 index 0000000..243a6df --- /dev/null +++ b/src/ssgs/hugo.js @@ -0,0 +1,27 @@ +import Ssg from './ssg.js'; + +export default class Hugo extends Ssg { + constructor() { + super('hugo'); + } + + configPaths() { + return super.configPaths().concat(['config.toml', 'hugo.toml', 'hugo.yaml', 'hugo.json']); + } + + templateExtensions() { + return []; + } + + partialFolders() { + return super.partialFolders().concat([ + 'archetypes/', // scaffolding templates + ]); + } + + ignoredFolders() { + return super.ignoredFolders().concat([ + 'resources/', // cache + ]); + } +} diff --git a/src/ssgs/jekyll.js b/src/ssgs/jekyll.js new file mode 100644 index 0000000..37d7362 --- /dev/null +++ b/src/ssgs/jekyll.js @@ -0,0 +1,77 @@ +import Ssg from './ssg.js'; + +export default class Jekyll extends Ssg { + constructor() { + super('jekyll'); + } + + configPaths() { + return super.configPaths().concat(['_config.yml', '_config.yaml']); + } + + templateExtensions() { + return super.templateExtensions().concat(['.liquid']); + } + + contentExtensions() { + return super.contentExtensions().concat(['.html']); + } + + partialFolders() { + return super.partialFolders().concat(['_layouts/']); + } + + ignoredFolders() { + return super.ignoredFolders().concat([ + '_site/', // build output + '.jekyll-cache/', // cache + '.jekyll-metadata/', // cache + ]); + } + + // _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 _files {import('../types').ParsedFiles} + * @param filePaths {string[]} List of input file paths. + * @param collectionPaths {{ basePath: string, paths: string[] }} + * @returns {string} + */ + getSource(_files, filePaths, collectionPaths) { + 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(_files, filePaths, collectionPaths); + } +} diff --git a/src/ssgs/next-js.js b/src/ssgs/next-js.js new file mode 100644 index 0000000..48c3465 --- /dev/null +++ b/src/ssgs/next-js.js @@ -0,0 +1,23 @@ +import Ssg from './ssg.js'; + +export default class NextJs extends Ssg { + constructor() { + super('nextjs'); + } + + configPaths() { + return super.configPaths().concat(['next.config.js', 'next.config.mjs']); + } + + templateExtensions() { + return super.templateExtensions().concat(['.tsx']); + } + + ignoredFolders() { + return super.ignoredFolders().concat([ + 'out/', // build output + '.next/', // cache + 'public/', // static assets + ]); + } +} diff --git a/src/ssgs/ssg.js b/src/ssgs/ssg.js new file mode 100644 index 0000000..793eb66 --- /dev/null +++ b/src/ssgs/ssg.js @@ -0,0 +1,175 @@ +import { extname } from 'path'; + +export default class Ssg { + /** @type {import('../types').SsgKey} */ + key; + + /** + * @param key {import('../types').SsgKey} + */ + constructor(key) { + this.key = key; + } + + /** + * @returns {string[]} + */ + configPaths() { + return []; + } + + /** + * @returns {string[]} + */ + templateExtensions() { + return ['.htm', '.html']; + } + + /** + * @returns {string[]} + */ + contentExtensions() { + return [ + '.md', + '.mdown', + '.markdown', + '.mdx', + '.json', + '.yml', + '.yaml', + '.toml', + '.csv', + '.tsv', + ]; + } + + /** + * @returns {string[]} + */ + partialFolders() { + return [ + 'layouts/', // general partials + 'components/', // general partials + 'component-library/', // general partials + 'schemas/', // CloudCannon schema files + ]; + } + + /** + * @returns {string[]} + */ + ignoredFolders() { + return []; + } + + /** + * Checks if the file at this path is an SSG configuration file. + * + * @param filePath {string} + * @returns {boolean} + */ + isConfigPath(filePath) { + return this.configPaths().some( + (configPath) => filePath === configPath || filePath.endsWith(`/${configPath}`), + ); + } + + /** + * Returns a score for how likely a file path relates to this SSG. + * + * @param filePath {string} + * @returns {number} + */ + getPathScore(filePath) { + return this.isConfigPath(filePath) ? 1 : 0; + } + + /** + * Checks if we should skip a file at this path + * + * @param filePath {string} + * @returns {boolean} + */ + isIgnoredPath(filePath) { + return this.ignoredFolders().some((folder) => filePath.startsWith(folder)); + } + + /** + * Checks if the file at this path is a contains Markdown or structured content. + * + * @param filePath {string} + * @returns {boolean} + */ + isContentPath(filePath) { + return this.contentExtensions().includes(extname(filePath)); + } + + /** + * Checks if the file at this path is an include, partial or layout file. + * + * @param filePath {string} + * @returns {boolean} + */ + isPartialPath(filePath) { + return this.partialFolders().some( + (partialFolder) => + filePath === partialFolder || + filePath.includes(`/${partialFolder}`) || + filePath.startsWith(partialFolder), + ); + } + + /** + * Checks if the file at this path is a template file. + * + * @param filePath {string} + * @returns boolean + */ + isTemplatePath(filePath) { + return this.templateExtensions().includes(extname(filePath)); + } + + /** + * Finds the likely type of the file at this path. + * + * @param filePath {string} + * @returns {import('../types').FileType} + */ + getFileType(filePath) { + if (this.isIgnoredPath(filePath)) { + return 'ignored'; + } + + if (this.isPartialPath(filePath)) { + return 'partial'; + } + + if (this.isConfigPath(filePath)) { + return 'config'; + } + + if (this.isContentPath(filePath)) { + return 'content'; + } + + if (this.isTemplatePath(filePath)) { + return 'template'; + } + + return 'other'; + } + + /** + * Attempts to find the most likely source folder. + * + * @param _files {import('../types').ParsedFiles} + * @param _filePaths {string[]} List of input file paths. + * @param collectionPaths {{ basePath: string, paths: string[] }} + * @returns {string} + */ + getSource(_files, _filePaths, collectionPaths) { + // This is the technically the collections path, which is often the source path. + // It's preferable to override this with a more accurate SSG-specific approach. + return collectionPaths.basePath; + } +} diff --git a/src/ssgs/ssgs.js b/src/ssgs/ssgs.js new file mode 100644 index 0000000..9418b21 --- /dev/null +++ b/src/ssgs/ssgs.js @@ -0,0 +1,49 @@ +import Bridgetown from './bridgetown.js'; +import Eleventy from './eleventy.js'; +import Hugo from './hugo.js'; +import Jekyll from './jekyll.js'; +import NextJs from './next-js.js'; +import Ssg from './ssg.js'; +import Sveltekit from './sveltekit.js'; + +const unknown = new Ssg('unknown'); + +const ssgs = [ + new Bridgetown(), + new Eleventy(), + new Hugo(), + new Jekyll(), + new NextJs(), + new Sveltekit(), + unknown, +]; + +/** + * Finds the most likely SSG for a set of files. + * + * @param filePaths {string[]} A list of file paths. + * @returns {Bridgetown | Eleventy | Hugo | Jekyll | NextJs | Sveltekit | Ssg} The assumed SSG. + */ +export function guessSsg(filePaths) { + /** @type {Record} */ + const scores = { + eleventy: 0, + jekyll: 0, + bridgetown: 0, + hugo: 0, + sveltekit: 0, + nextjs: 0, + unknown: 0, + }; + + for (let i = 0; i < filePaths.length; i++) { + for (let j = 0; j < ssgs.length; j++) { + scores[ssgs[j].key] += ssgs[j].getPathScore(filePaths[i]); + } + } + + return ssgs.reduce( + (previous, current) => (scores[previous.key] < scores[current.key] ? current : previous), + unknown, + ); +} diff --git a/src/ssgs/sveltekit.js b/src/ssgs/sveltekit.js new file mode 100644 index 0000000..578ee9b --- /dev/null +++ b/src/ssgs/sveltekit.js @@ -0,0 +1,27 @@ +import Ssg from './ssg.js'; + +export default class Sveltekit extends Ssg { + constructor() { + super('sveltekit'); + } + + configPaths() { + return super.configPaths().concat(['svelte.config.js']); + } + + templateExtensions() { + return super.templateExtensions().concat(['.svelte']); + } + + contentExtensions() { + return super.contentExtensions().concat(['.svx']); + } + + ignoredFolders() { + return super.ignoredFolders().concat([ + 'build/', // build output + '.svelte-kit/', // cache + 'static/', // static assets + ]); + } +} diff --git a/src/types.d.ts b/src/types.d.ts index d201cfe..5195bc6 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -9,17 +9,28 @@ export type SsgKey = | 'nextjs' | 'unknown'; -interface FileSummary { +export interface ParsedFile { filePath: string; type: FileType; collectionPaths?: string[]; } -interface GenerateOptions { +export interface GenerateOptions { /** Custom user configuration. */ - userConfig?: Record; + userConfig?: { + source?: string; + }; /** SSG-specific build configuration. */ - buildConfig?: Record; + buildConfig?: { + source?: string; + }; /** Function to access the source contents a file. */ readFile?: (path: string) => Promise; } + +export interface ParsedFiles { + groups: Record; + collectionPathCounts: Record; +} + +export type CollectionsConfig = Record; diff --git a/src/utility.js b/src/utility.js index e159b0f..50b0660 100644 --- a/src/utility.js +++ b/src/utility.js @@ -8,3 +8,18 @@ export function last(array) { return array[array.length - 1]; } + +/** + * Removes the first section of a path if it exists. + * + * @param path {string} + * @param stripPath {string} + * @returns {string} + */ +export function stripTopPath(path, stripPath) { + if (path === stripPath) { + return ''; + } + + return path.startsWith(`${stripPath}/`) ? path.substring(stripPath.length + 1) : path; +} diff --git a/test/collections.test.js b/test/collections.test.js new file mode 100644 index 0000000..2c1abb2 --- /dev/null +++ b/test/collections.test.js @@ -0,0 +1,16 @@ +import test from 'ava'; +import { processCollectionPaths } from '../src/collections.js'; + +test('processes collection paths', (t) => { + const processed = processCollectionPaths({ + src: 1, + 'src/_data': 3, + 'src/_includes': 3, + 'src/_includes/nav': 2, + }); + + t.deepEqual(processed, { + basePath: 'src', + paths: ['', '_data', '_includes', '_includes/nav'], + }); +}); diff --git a/test/ssgs/jekyll.test.js b/test/ssgs/jekyll.test.js new file mode 100644 index 0000000..fa83b1b --- /dev/null +++ b/test/ssgs/jekyll.test.js @@ -0,0 +1,15 @@ +import test from 'ava'; +import Jekyll from '../../src/ssgs/jekyll.js'; + +test('gets source path from convention path', (t) => { + const jekyll = new Jekyll(); + const filePaths = [ + '_config.yml', + 'sauce/_drafts/wip.md', + 'sauce/index.html', + 'sauce/_includes/header.html', + 'sauce/_sass/_typography.scss', + ]; + + t.deepEqual(jekyll.getSource(undefined, filePaths, { basePath: 'salsa' }), 'sauce'); +}); diff --git a/test/utility.test.js b/test/utility.test.js index 2ed9917..46e7efb 100644 --- a/test/utility.test.js +++ b/test/utility.test.js @@ -1,8 +1,14 @@ import test from 'ava'; -import { last } from '../src/utility.js'; +import { last, stripTopPath } from '../src/utility.js'; test('gets last element', (t) => { t.is(last(['first', 'final']), 'final'); t.is(last(['first']), 'first'); t.is(last([]), undefined); }); + +test('strips top path', (t) => { + t.is(stripTopPath('src/content/index.html', 'src'), 'content/index.html'); + t.is(stripTopPath('src/content/index.html', ''), 'src/content/index.html'); + t.is(stripTopPath('src', 'src'), ''); +});