@cloudcannon/reader
Advanced tools
Comparing version 0.0.5 to 0.1.0-0
{ | ||
"name": "@cloudcannon/reader", | ||
"type": "module", | ||
"version": "0.0.5", | ||
"version": "0.1.0-0", | ||
"description": "Parses config, files and folder structures to create a JSON file with information about sites made with any static site generator.", | ||
@@ -25,3 +25,4 @@ "keywords": [ | ||
"lint": "eslint src", | ||
"release": "npx np" | ||
"release:latest": "npx np", | ||
"release:next": "npx np --tag=next" | ||
}, | ||
@@ -47,2 +48,3 @@ "bin": { | ||
"@sindresorhus/slugify": "^2.1.0", | ||
"chalk": "^4.1.2", | ||
"cosmiconfig": "^7.0.1", | ||
@@ -54,2 +56,3 @@ "fdir": "^5.1.0", | ||
"papaparse": "^5.3.1", | ||
"picomatch": "^2.3.0", | ||
"properties": "^1.2.1", | ||
@@ -56,0 +59,0 @@ "toml": "^3.0.0" |
@@ -31,2 +31,4 @@ # Reader | ||
To print usage details: | ||
```bash | ||
@@ -39,4 +41,6 @@ $ cloudcannon-reader --help | ||
Options | ||
--version Print the current version | ||
--config, -c Use a specific configuration file | ||
--output, -o Write to a different location than . | ||
--quiet, -q Disable logging | ||
@@ -83,2 +87,5 @@ Examples | ||
// Tells CloudCannon this collection produces output files | ||
output: true | ||
// CloudCannon collection-level configuration | ||
@@ -100,4 +107,21 @@ name: 'Personnel', | ||
return `/posts/${year}/${slug}/`; | ||
} | ||
}, | ||
// Tells CloudCannon this collection produces output files | ||
output: true | ||
}, | ||
pages: { | ||
// Tells CloudCannon to navigate to this path for this collection | ||
path: '', | ||
// Reads the contents of each file for this pattern (takes priority over path) | ||
glob: ['**/*.md', './src/pages/*.html'], | ||
// Tells CloudCannon to only show successfully parsed files for this collection | ||
// Useful for excluding other collections when using '' as path | ||
filter: 'strict', | ||
// Tells CloudCannon this collection produces output files | ||
output: true | ||
}, | ||
data: { | ||
@@ -212,2 +236,23 @@ // Reads the contents of each file in this directory | ||
<details> | ||
<summary><code>glob</code> (optional)</summary> | ||
> The `glob` is a string or array of strings containing patterns to filter the files parsed into this collection. Globs are **not** relative to `source`. Patterns are matched with [picomatch](https://github.com/micromatch/picomatch#basic-globbing). If set as an array, files only have to match one glob pattern to be parsed. | ||
> | ||
> ```javascript | ||
> glob: ['**/*.md', '**/*.html'] // All .md and .html files | ||
> ``` | ||
> | ||
> ```javascript | ||
> glob: './src/**/*.liquid' // All .liquid files inside the src folder and subfolders | ||
> ``` | ||
> | ||
> This is used to find files instead of `path`, but path is still required as a base path for the collection. | ||
> | ||
> - `'./src/*.md'` matches `.md` files in the `src` folder. | ||
> - `'**/*.html'` matches `.html` files in any folder or subfolder. | ||
> - `['**/*.md', './pages/*.html']` matches `.md` files in any folder, or `.html` files in the `pages` folder. | ||
</details> | ||
<details> | ||
<summary><code>url</code> (optional)</summary> | ||
@@ -217,6 +262,22 @@ | ||
> | ||
> Functions are are supported with `.js` or `.cjs` files. Given file path, parsed file content and an object with filters as arguments. The return value should be the slash-prefixed URL string. | ||
> Functions are are supported with `.js` or `.cjs` files. Given file path, parsed file content and an object with filters and the `buildUrl` function as arguments. The return value should be the slash-prefixed URL string. | ||
> | ||
> ```javascript | ||
> url: (filePath, content, { filters, buildUrl }) => { | ||
> if (content.permalink) { | ||
> // Returns a lower case permalink front matter field | ||
> return filters.lowercase(content.permalink); | ||
> } | ||
> | ||
> // Falls back to processing a url string | ||
> return buildUrl(filePath, content, '/[slug]/'); | ||
> } | ||
> ``` | ||
> | ||
> Strings are used as a template to build the URL. There are two types of placeholders available, file and data. Placeholders resulting in empty values are supported. Sequential slashes in URLs are condensed to one. | ||
> | ||
> ```javascript | ||
> url: '/blog/{date|year}/[slug]/' | ||
> ``` | ||
> | ||
> File placeholders are always available, and provided by `cloudcannon-reader`: | ||
@@ -235,2 +296,5 @@ > | ||
> - `{tag|slugify|uppercase}` is `tag` from inside the file, slugified, then upper cased. | ||
> - `{date|year}` is `date` from inside the file, with the 4-digit year extracted. | ||
> - `{date|month}` is `date` from inside the file, with the 2-digit month extracted. | ||
> - `{date|day}` is `date` from inside the file, with the 2-digit day extracted. | ||
@@ -243,2 +307,6 @@ </details> | ||
> The `parser` field should state which [Parser](#parsers) you want to use to read the files in this collection. | ||
> | ||
> ```javascript | ||
> parser: 'front-matter' | ||
> ``` | ||
@@ -245,0 +313,0 @@ </details> |
@@ -0,4 +1,6 @@ | ||
import chalk from 'chalk'; | ||
import { fdir } from 'fdir'; | ||
import { join } from 'path'; | ||
import { buildUrl } from '../util/url-builder.js'; | ||
import log from '../util/logger.js'; | ||
import { parseFile } from '../parsers/parser.js'; | ||
@@ -16,25 +18,42 @@ | ||
async function readCollectionItem(filePath, collectionConfig, key, source) { | ||
const data = await parseFile(filePath, collectionConfig.parser); | ||
const itemPath = source && filePath.startsWith(source) | ||
? filePath.slice(source.length + 1) // +1 for slash after source | ||
: filePath; | ||
try { | ||
const data = await parseFile(filePath, collectionConfig.parser); | ||
const itemPath = source && filePath.startsWith(source) | ||
? filePath.slice(source.length + 1) // +1 for slash after source | ||
: filePath; | ||
return { | ||
...data, | ||
path: itemPath, | ||
collection: key, | ||
url: buildUrl(itemPath, data, collectionConfig.url) | ||
}; | ||
return { | ||
...data, | ||
path: itemPath, | ||
collection: key, | ||
url: buildUrl(itemPath, data, collectionConfig.url) | ||
}; | ||
} catch (e) { | ||
log(` ${chalk.bold(filePath)} skipped due to ${chalk.red(e.message)}`); | ||
} | ||
} | ||
async function readCollection(collectionConfig, key, source) { | ||
const filePaths = await new fdir() | ||
const crawler = new fdir() | ||
.withBasePath() | ||
.filter((filePath, isDirectory) => !isDirectory && !filePath.includes('/_defaults.')) | ||
.filter((filePath, isDirectory) => !isDirectory && !filePath.includes('/_defaults.')); | ||
const glob = typeof collectionConfig.glob === 'string' | ||
? [collectionConfig.glob] | ||
: collectionConfig.glob; | ||
if (collectionConfig.glob) { | ||
crawler.glob(glob); | ||
} | ||
const filePaths = await crawler | ||
.crawl(join(source, collectionConfig.path)) | ||
.withPromise(); | ||
return await Promise.all(filePaths.map(async (filePath) => { | ||
return await readCollectionItem(filePath, collectionConfig, key, source); | ||
})); | ||
return await filePaths.reduce(async (memo, filePath) => { | ||
const collectionItem = await readCollectionItem(filePath, collectionConfig, key, source); | ||
return collectionItem | ||
? [...(await memo), collectionItem] | ||
: await memo; | ||
}, []); | ||
} |
@@ -5,2 +5,3 @@ #!/usr/bin/env node | ||
import runner from './runner.js'; | ||
import { toggleLogging } from './util/logger.js'; | ||
@@ -15,2 +16,3 @@ const cli = meow(` | ||
--output, -o Write to a different location than . | ||
--quiet, -q Disable logging | ||
@@ -30,2 +32,6 @@ Examples | ||
alias: 'o' | ||
}, | ||
quiet: { | ||
quiet: 'string', | ||
alias: 'q' | ||
} | ||
@@ -35,3 +41,6 @@ } | ||
runner.run(cli.flags, cli.pkg) | ||
.then(() => console.log('Generated info.json successfully.')); | ||
if (cli.flags.quiet) { | ||
toggleLogging(false); | ||
} | ||
runner.run(cli.flags, cli.pkg); |
@@ -45,3 +45,3 @@ import { readFile } from 'fs/promises'; | ||
if (!parse) { | ||
throw new Error(`Unsupported parser: ${parser}`); | ||
throw new Error(parser ? `unsupported parser ${parser}` : 'unknown parser'); | ||
} | ||
@@ -48,0 +48,0 @@ |
import { mkdir, writeFile } from 'fs/promises'; | ||
import { join } from 'path'; | ||
import { join, relative } from 'path'; | ||
import { cosmiconfig } from 'cosmiconfig'; | ||
import { generateInfo } from './generators/info.js'; | ||
import log from './util/logger.js'; | ||
import report from './util/reporter.js'; | ||
import chalk from 'chalk'; | ||
@@ -26,11 +29,18 @@ export default { | ||
if (config) { | ||
console.log(`Using config file: ${config.filepath}`); | ||
const relativeConfigPath = relative(process.cwd(), config.filepath); | ||
log(`⚙️ Using config file at ${chalk.bold(relativeConfigPath)}`); | ||
return config.config || {}; | ||
} | ||
} catch (e) { | ||
console.error(e); | ||
if (e.code === 'ENOENT') { | ||
log(`⚠️ ${chalk.red('No config file found at')} ${chalk.red.bold(configPath)}`); | ||
return false; | ||
} else { | ||
log(`⚠️ ${chalk.red('Error reading config file')}`, 'error'); | ||
throw e; | ||
} | ||
} | ||
console.log('No config file found.'); | ||
return {}; | ||
log('⚙️ No config file found, see the instructions at https://github.com/CloudCannon/reader'); | ||
return false; | ||
}, | ||
@@ -42,11 +52,19 @@ | ||
write: async function (info, outputDir) { | ||
write: async function (info, outputDir, outputPath) { | ||
await mkdir(outputDir, { recursive: true }); | ||
await writeFile(join(outputDir, 'info.json'), JSON.stringify(info, null, '\t')); | ||
await writeFile(outputPath, JSON.stringify(info, null, '\t')); | ||
}, | ||
run: async function (flags, pkg) { | ||
const config = await this.readConfig(flags?.config) || {}; | ||
log(`⭐️ Starting ${chalk.blue('cloudcannon-reader')}`); | ||
const config = await this.readConfig(flags?.config); | ||
if (config === false) { | ||
return; | ||
} | ||
config.output = flags?.output || config.output; | ||
const outputDir = join('.', config.output || '', '_cloudcannon'); | ||
const outputPath = join(outputDir, 'info.json'); | ||
let info; | ||
@@ -57,3 +75,3 @@ | ||
} catch (e) { | ||
e.message = `Failed to generate info: ${e.message}`; | ||
log(`⚠️ ${chalk.red('Failed to generate')} ${chalk.red.bold(outputPath)}`, 'error'); | ||
throw e; | ||
@@ -63,6 +81,7 @@ } | ||
try { | ||
const outputDir = join('.', config.output || '', '_cloudcannon'); | ||
await this.write(info, outputDir); | ||
await this.write(info, outputDir, outputPath); | ||
report(info); | ||
log(`🏁 Generated ${chalk.bold(outputPath)} ${chalk.green('successfully')}`); | ||
} catch (e) { | ||
e.message = `Failed to write info: ${e.message}`; | ||
log(`⚠️ ${chalk.red('Failed to write')} ${chalk.red.bold(outputPath)}`, 'error'); | ||
throw e; | ||
@@ -69,0 +88,0 @@ } |
@@ -7,3 +7,12 @@ import { parse } from 'path'; | ||
lowercase: (value) => value?.toLowerCase?.(), | ||
slugify | ||
slugify, | ||
year: (value) => value?.getFullYear?.(), | ||
month: (value) => { | ||
const month = value?.getMonth?.(); | ||
return month === undefined ? month : ('0' + (month + 1)).slice(-2); | ||
}, | ||
day: (value) => { | ||
const day = value?.getDate?.(); | ||
return day === undefined ? day : ('0' + day).slice(-2); | ||
} | ||
}; | ||
@@ -40,3 +49,3 @@ | ||
if (typeof urlTemplate === 'function') { | ||
return urlTemplate(filePath, data, { filters }); | ||
return urlTemplate(filePath, data, { filters, buildUrl }); | ||
} | ||
@@ -43,0 +52,0 @@ |
24875
18
372
377
11
+ Addedchalk@^4.1.2
+ Addedpicomatch@^2.3.0
+ Addedansi-styles@4.3.0(transitive)
+ Addedchalk@4.1.2(transitive)
+ Addedcolor-convert@2.0.1(transitive)
+ Addedcolor-name@1.1.4(transitive)
+ Addedhas-flag@4.0.0(transitive)
+ Addedpicomatch@2.3.1(transitive)
+ Addedsupports-color@7.2.0(transitive)