From c144271b0b4f9a2d0750ddb99a32e7a0844fb8e8 Mon Sep 17 00:00:00 2001 From: Ross Phillips <12723297+rphillips-cc@users.noreply.github.com> Date: Fri, 2 Aug 2024 11:52:16 +1200 Subject: [PATCH 1/2] Update types --- package-lock.json | 57 ++++++++++----------- package.json | 6 +-- src/index.js | 105 +++++++++++---------------------------- src/ssgs/jekyll.js | 90 ++++++++++++++++++++++++++++++--- src/ssgs/ssg.js | 63 +++++++++++++++++++++-- src/types.d.ts | 14 ++++-- test/ssgs/jekyll.test.js | 2 +- 7 files changed, 212 insertions(+), 125 deletions(-) diff --git a/package-lock.json b/package-lock.json index a778931..608d6a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,11 @@ "titleize": "^4.0.0" }, "devDependencies": { - "@cloudcannon/configuration-types": "^0.0.7", - "@types/node": "^20.14.12", + "@cloudcannon/configuration-types": "^0.0.8", + "@types/node": "^22.0.2", "ava": "^6.1.3", "c8": "^10.1.2", - "eslint": "^9.7.0", + "eslint": "^9.8.0", "prettier": "^3.3.3", "typescript": "^5.5.4" } @@ -39,9 +39,9 @@ "dev": true }, "node_modules/@cloudcannon/configuration-types": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@cloudcannon/configuration-types/-/configuration-types-0.0.7.tgz", - "integrity": "sha512-MO8GsArzegxVNJ20bSawulAOregtFm7GHnDI+gMF7gEMRaTxB72JsCgIdqi/lkOrQKgRac3lGcUPINzlXnufgw==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@cloudcannon/configuration-types/-/configuration-types-0.0.8.tgz", + "integrity": "sha512-pw+jfembGX3C3HSaPquL+7PJj8Z3HMkqhMWypjUdM9LNGX9gBW3jbDpgDojRiW1YEwxNqqewzLiaTG8/Ip1vmQ==", "dev": true, "dependencies": { "@cloudcannon/snippet-types": "^1.1.11", @@ -116,9 +116,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.7.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.7.0.tgz", - "integrity": "sha512-ChuWDQenef8OSFnvuxv0TCVxEwmu3+hPNKvM9B34qpM0rDRbjL8t5QkQeHHeAfsKQjuH9wS82WeCi1J/owatng==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", + "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -440,12 +440,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", - "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.2.tgz", + "integrity": "sha512-yPL6DyFwY5PiMVEwymNeqUTKsDczQBJ/5T7W/46RwLU/VH+AA8aT5TZkvBviLKLbbm0hlfftEkGrNzfRk/fofQ==", "dev": true, "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.11.1" } }, "node_modules/@vercel/nft": { @@ -1151,16 +1151,16 @@ } }, "node_modules/eslint": { - "version": "9.7.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.7.0.tgz", - "integrity": "sha512-FzJ9D/0nGiCGBf8UXO/IGLTgLVzIxze1zpfA8Ton2mjLovXdAPlYDv+MQDcqj3TmrhAGYfOpz9RfR+ent0AgAw==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", + "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.17.0", + "@eslint/config-array": "^0.17.1", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.7.0", + "@eslint/js": "9.8.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -3304,9 +3304,9 @@ } }, "node_modules/ts-json-schema-generator/node_modules/glob": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", - "integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", @@ -3319,17 +3319,14 @@ "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/ts-json-schema-generator/node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -3382,9 +3379,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz", + "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==", "dev": true }, "node_modules/unicorn-magic": { diff --git a/package.json b/package.json index f09d22b..f2ebc77 100644 --- a/package.json +++ b/package.json @@ -36,11 +36,11 @@ }, "author": "CloudCannon ", "devDependencies": { - "@cloudcannon/configuration-types": "^0.0.7", - "@types/node": "^20.14.12", + "@cloudcannon/configuration-types": "^0.0.8", + "@types/node": "^22.0.2", "ava": "^6.1.3", "c8": "^10.1.2", - "eslint": "^9.7.0", + "eslint": "^9.8.0", "prettier": "^3.3.3", "typescript": "^5.5.4" }, diff --git a/src/index.js b/src/index.js index eaa1aeb..a6bb05c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,74 +1,24 @@ import { guessSsg, ssgs } from './ssgs/ssgs.js'; -import { last, stripTopPath } from './utility.js'; -import { getCollectionPaths, processCollectionPaths } from './collections.js'; +import { stripTopPath } from './utility.js'; +import { processCollectionPaths } from './collections.js'; export { ssgs } from './ssgs/ssgs.js'; /** - * Provides a summary of a file at this path. + * Filters out file paths not in the provided source. * - * @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 parseFile(filePath, ssg) { - const type = ssg.getFileType(filePath); - - return { - filePath, - type, - }; -} - -/** - * Provides a summary of files. - * - * @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. + * @param filePaths {string[]} List of input file paths. + * @param source {string | undefined} The source path. */ -function parseFiles(filePaths, ssg) { - /** @type {Record} */ - const collectionPathCounts = {}; - - /** @type {Record} */ - const groups = { - config: [], - content: [], - partial: [], - other: [], - template: [], - ignored: [], - }; - - for (let i = 0; i < filePaths.length; i++) { - const file = parseFile(filePaths[i], ssg); - - if (file.type === 'content') { - const lastPath = last(getCollectionPaths(filePaths[i])); - if (lastPath || lastPath === '') { - collectionPathCounts[lastPath] = collectionPathCounts[lastPath] || 0; - collectionPathCounts[lastPath] += 1; - } - } +function filterPaths(filePaths, source) { + source = `${source || ''}/`.replace(/\/+/, '/').replace(/^\//, ''); + source = source === '/' ? '' : source; - if (file.type !== 'ignored') { - groups[file.type].push(file); - } + if (!source) { + return filePaths; } - return { collectionPathCounts, groups }; -} - -/** - * Attempts to find the current timezone. - * - * @returns {import('@cloudcannon/configuration-types').Timezone | undefined} - */ -function getTimezone() { - const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - return /** @type {import('@cloudcannon/configuration-types').Timezone | undefined} */ (timezone); + return filePaths.filter((filePath) => filePath.startsWith(source)); } /** @@ -76,30 +26,33 @@ function getTimezone() { * * @param filePaths {string[]} List of input file paths. * @param options {import('./types').GenerateOptions=} Options to aid generation. - * @returns {Promise} + * @returns {Promise} */ export async function generate(filePaths, options) { - const ssgKey = options?.config?.ssg || options?.buildConfig?.ssg; - const ssg = ssgKey ? ssgs[ssgKey] : guessSsg(filePaths); - const files = parseFiles(filePaths, ssg); - const collectionPaths = processCollectionPaths(files.collectionPathCounts); + const ssg = options?.buildConfig?.ssg + ? ssgs[options.buildConfig.ssg] + : guessSsg(filterPaths(filePaths, options?.config?.source)); - const source = - options?.config?.source ?? options?.buildConfig?.source ?? ssg.getSource(files, filePaths); + const source = options?.config?.source ?? ssg.getSource(filePaths); + filePaths = filterPaths(filePaths, source); + const files = ssg.groupFiles(filePaths); + const collectionPaths = processCollectionPaths(files.collectionPathCounts); const collectionsConfig = options?.config?.collections_config || ssg.generateCollectionsConfig(collectionPaths, source); return { ssg: ssg?.key, - source, - collections_config: collectionsConfig, - paths: { - collections: source - ? stripTopPath(collectionPaths.basePath, source) - : collectionPaths.basePath, - ...options?.config?.paths, + config: { + source, + collections_config: collectionsConfig, + paths: { + collections: source + ? stripTopPath(collectionPaths.basePath, source) + : collectionPaths.basePath, + ...options?.config?.paths, + }, + timezone: options?.config?.timezone ?? ssg.getTimezone(), }, - timezone: options?.config?.timezone ?? getTimezone(), }; } diff --git a/src/ssgs/jekyll.js b/src/ssgs/jekyll.js index 7481c5f..a83496e 100644 --- a/src/ssgs/jekyll.js +++ b/src/ssgs/jekyll.js @@ -1,5 +1,25 @@ import Ssg from './ssg.js'; +/** + * Checks if this is a Jekyll drafts collection path. + * + * @param path {string | undefined} The path to check. + * @returns {boolean} + */ +function isDraftsPath(path) { + return !!path?.match(/\b_drafts$/); +} + +/** + * Checks if this is a Jekyll posts collection path. + * + * @param path {string | undefined} The path to check. + * @returns {boolean} + */ +function isPostsPath(path) { + return !!path?.match(/\b_posts$/); +} + export default class Jekyll extends Ssg { constructor() { super('jekyll'); @@ -59,12 +79,10 @@ export default class Jekyll extends Ssg { /** * 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 | undefined} */ - getSource(_files, filePaths) { + getSource(filePaths) { const { filePath, conventionPath } = this._findConventionPath(filePaths); if (filePath && conventionPath) { @@ -72,7 +90,7 @@ export default class Jekyll extends Ssg { return filePath.substring(0, Math.max(0, conventionIndex - 1)); } - return super.getSource(_files, filePaths); + return super.getSource(filePaths); } /** @@ -80,13 +98,71 @@ export default class Jekyll extends Ssg { * * @param key {string} * @param path {string} - * @param basePath {string} * @returns {import('@cloudcannon/configuration-types').CollectionConfig} */ - generateCollectionConfig(key, path, basePath) { - const collectionConfig = super.generateCollectionConfig(key, path, basePath); + generateCollectionConfig(key, path) { + const collectionConfig = super.generateCollectionConfig(key, path); // TODO: read contents of _config.yml to find which collections are output collectionConfig.output = key !== 'data'; + + 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'), + }, + ]; + } + + if (isDraftsPath(collectionConfig.path)) { + collectionConfig.create ||= { + path: '', // TODO: this should not be required if publish_to is set + publish_to: key.replace('drafts', 'posts'), + }; + } + return collectionConfig; } + + /** + * Generates collections config from a set of paths. + * + * @param collectionPaths {{ basePath: string, paths: string[] }} + * @param source {string | undefined} + * @returns {import('../types').CollectionsConfig} + */ + generateCollectionsConfig(collectionPaths, source) { + const collectionsConfig = super.generateCollectionsConfig(collectionPaths, source); + + const keys = Object.keys(collectionsConfig); + + for (const key of keys) { + const collectionConfig = collectionsConfig[key]; + + if (isDraftsPath(collectionConfig.path) && collectionConfig.path) { + // Ensure there is a matching posts collection + const postsKey = key.replace('drafts', 'posts'); + collectionsConfig[postsKey] ||= this.generateCollectionConfig( + postsKey, + collectionConfig.path?.replace(/\b_drafts$/, '_posts'), + ); + } else if (isPostsPath(collectionConfig.path) && collectionConfig.path) { + // Ensure there is a matching drafts collection + const draftsKey = key.replace('posts', 'drafts'); + collectionsConfig[draftsKey] ||= this.generateCollectionConfig( + draftsKey, + collectionConfig.path?.replace(/\b_posts$/, '_drafts'), + ); + } + } + + return collectionsConfig; + } } diff --git a/src/ssgs/ssg.js b/src/ssgs/ssg.js index 979f422..e3e2223 100644 --- a/src/ssgs/ssg.js +++ b/src/ssgs/ssg.js @@ -3,7 +3,8 @@ import { basename } from 'path'; import slugify from '@sindresorhus/slugify'; import titleize from 'titleize'; import { findIcon } from '../icons.js'; -import { stripTopPath } from '../utility.js'; +import { last, stripTopPath } from '../utility.js'; +import { getCollectionPaths } from '../collections.js'; export default class Ssg { /** @type {import('@cloudcannon/configuration-types').SsgKey} */ @@ -16,6 +17,60 @@ export default class Ssg { this.key = key || 'unknown'; } + /** + * Provides a summary of files. + * + * @param filePaths {string[]} The input file path. + * @returns {import('../types').GroupedFileSummaries} The file summaries grouped by type. + */ + groupFiles(filePaths) { + /** @type {Record} */ + const collectionPathCounts = {}; + + /** @type {Record} */ + const groups = { + config: [], + content: [], + partial: [], + other: [], + template: [], + ignored: [], + }; + + for (let i = 0; i < filePaths.length; i++) { + const summary = { + filePath: filePaths[i], + type: this.getFileType(filePaths[i]), + }; + + if (summary.type === 'content') { + const lastPath = last(getCollectionPaths(filePaths[i])); + if (lastPath || lastPath === '') { + collectionPathCounts[lastPath] = collectionPathCounts[lastPath] || 0; + collectionPathCounts[lastPath] += 1; + } + } + + if (summary.type !== 'ignored') { + groups[summary.type].push(summary); + } + } + + return { collectionPathCounts, groups }; + } + + /** + * Attempts to find the current timezone. + * + * @returns {import('@cloudcannon/configuration-types').Timezone | undefined} + */ + getTimezone() { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + return /** @type {import('@cloudcannon/configuration-types').Timezone | undefined} */ ( + timezone + ); + } + /** * @returns {string[]} */ @@ -87,6 +142,7 @@ export default class Ssg { '.prettierrc.json', 'package-lock.json', 'package.json', + 'manifest.json', '.gitignore', 'README', 'README.md', @@ -206,11 +262,10 @@ export default class Ssg { /** * Attempts to find the most likely source folder. * - * @param _files {import('../types').ParsedFiles} * @param _filePaths {string[]} List of input file paths. * @returns {string | undefined} */ - getSource(_files, _filePaths) { + getSource(_filePaths) { return; } @@ -219,7 +274,7 @@ export default class Ssg { * * @param key {string} * @param path {string} - * @param _basePath {string} + * @param _basePath {string=} * @returns {import('@cloudcannon/configuration-types').CollectionConfig} */ generateCollectionConfig(key, path, _basePath) { diff --git a/src/types.d.ts b/src/types.d.ts index 886a822..bbbcc9d 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -6,7 +6,7 @@ import { export type FileType = 'config' | 'content' | 'template' | 'partial' | 'ignored' | 'other'; -export interface ParsedFile { +export interface FileSummary { filePath: string; type: FileType; collectionPaths?: string[]; @@ -18,14 +18,20 @@ export interface GenerateOptions { /** Build configuration, most likely the parsed CLI options for specific SSGs. */ buildConfig?: { ssg?: SsgKey; - source?: string; }; /** Function to access the source contents a file. */ readFile?: (path: string) => Promise; } -export interface ParsedFiles { - groups: Record; +export interface GenerateResult { + /** Identifies what SSG was used during config generation. */ + ssg: SsgKey | undefined; + /** The generated configuration. */ + config: Configuration; +} + +export interface GroupedFileSummaries { + groups: Record; collectionPathCounts: Record; } diff --git a/test/ssgs/jekyll.test.js b/test/ssgs/jekyll.test.js index fa83b1b..05a0437 100644 --- a/test/ssgs/jekyll.test.js +++ b/test/ssgs/jekyll.test.js @@ -11,5 +11,5 @@ test('gets source path from convention path', (t) => { 'sauce/_sass/_typography.scss', ]; - t.deepEqual(jekyll.getSource(undefined, filePaths, { basePath: 'salsa' }), 'sauce'); + t.deepEqual(jekyll.getSource(filePaths), 'sauce'); }); From f125687ee15a88b73c551c34ff0b5fe53c2ab5a8 Mon Sep 17 00:00:00 2001 From: Ross Phillips <12723297+rphillips-cc@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:27:18 +1200 Subject: [PATCH 2/2] fix: tests --- src/index.js | 2 +- toolproof_tests/demo.toolproof.yml | 24 ++++++++++--------- .../refs_example/demo_test.toolproof.yml | 24 ++++++++++--------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/index.js b/src/index.js index a6bb05c..75bc09f 100644 --- a/src/index.js +++ b/src/index.js @@ -42,7 +42,7 @@ export async function generate(filePaths, options) { options?.config?.collections_config || ssg.generateCollectionsConfig(collectionPaths, source); return { - ssg: ssg?.key, + ssg: ssg.key, config: { source, collections_config: collectionsConfig, diff --git a/toolproof_tests/demo.toolproof.yml b/toolproof_tests/demo.toolproof.yml index 2438570..1f42a74 100644 --- a/toolproof_tests/demo.toolproof.yml +++ b/toolproof_tests/demo.toolproof.yml @@ -57,15 +57,17 @@ steps: snapshot_content: |- ╎{ ╎ "ssg": "eleventy", - ╎ "collections_config": { - ╎ "staff_members": { - ╎ "path": "staff-members", - ╎ "name": "Staff Members", - ╎ "icon": "card_membership" - ╎ } - ╎ }, - ╎ "paths": { - ╎ "collections": "" - ╎ }, - ╎ "timezone": "Pacific/Auckland" + ╎ "config": { + ╎ "collections_config": { + ╎ "staff_members": { + ╎ "path": "staff-members", + ╎ "name": "Staff Members", + ╎ "icon": "card_membership" + ╎ } + ╎ }, + ╎ "paths": { + ╎ "collections": "" + ╎ }, + ╎ "timezone": "Pacific/Auckland" + ╎ } ╎} diff --git a/toolproof_tests/refs_example/demo_test.toolproof.yml b/toolproof_tests/refs_example/demo_test.toolproof.yml index 6a9e2ce..224dfd6 100644 --- a/toolproof_tests/refs_example/demo_test.toolproof.yml +++ b/toolproof_tests/refs_example/demo_test.toolproof.yml @@ -21,15 +21,17 @@ steps: snapshot_content: |- ╎{ ╎ "ssg": "eleventy", - ╎ "collections_config": { - ╎ "staff_members": { - ╎ "path": "staff-members", - ╎ "name": "Staff Members", - ╎ "icon": "card_membership" - ╎ } - ╎ }, - ╎ "paths": { - ╎ "collections": "" - ╎ }, - ╎ "timezone": "Pacific/Auckland" + ╎ "config": { + ╎ "collections_config": { + ╎ "staff_members": { + ╎ "path": "staff-members", + ╎ "name": "Staff Members", + ╎ "icon": "card_membership" + ╎ } + ╎ }, + ╎ "paths": { + ╎ "collections": "" + ╎ }, + ╎ "timezone": "Pacific/Auckland" + ╎ } ╎}