diff --git a/.gitignore b/.gitignore index 22cc89a..449ebca 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ node_modules /bs-manifest.json /bs-cache /pages.json +/posts.json /if-* /twitter-cache.json /twitter-config.json diff --git a/build/file.js b/build/file.js index 4b412df..498cfcd 100644 --- a/build/file.js +++ b/build/file.js @@ -28,19 +28,19 @@ module.exports = exports = class File { file.basename = basename = basename.slice(1); } - // remove the public root and any _images segment from the dir - const dir = this._dir(file.dir); - this.kind = kind(filepath); this.type = type(filepath); + this.input = filepath; // public/file.ext this.cwd = file.dir; this.ext = this.preprocessed ? file.ext : normalizedExt(file.ext); - this.input = filepath; // public/file.ext - this.base = path.join(...dir); // '', 'folder', 'folder/subfolder' - this.dir = path.join('/', ...dir); // /, /folder, /folder/subfolder this.name = name; // index, fileA, fileB this.basename = basename; // index.ext, fileA.ext, fileB.ext - this.ext = file.ext; + + const dir = this._dir(file.dir); + if (dir) { + this.base = path.join(...dir); // '', 'folder', 'folder/subfolder' + this.dir = path.join('/', ...dir); // /, /folder, /folder/subfolder + } this._out(); diff --git a/build/files.js b/build/files.js new file mode 100644 index 0000000..a5d20a9 --- /dev/null +++ b/build/files.js @@ -0,0 +1,71 @@ +const path = require('path'); +const { groupBy, keyBy, filter, find, get, memoize } = require('lodash'); +const { kind, KIND } = require('./resolve'); +const File = require('./file'); +const Asset = require('./asset'); +const Page = require('./page'); + +module.exports = exports = class Files { + + constructor (paths, base = '') { + this.KIND_MAP = this._kindMap(); + + this.base = base; + this.files = paths.map(this._parsePath.bind(this)).filter(Boolean); + + const { + [KIND.PAGE]: pages, + [KIND.ASSET]: assets, + } = groupBy(this.files, 'kind'); + + this.pages = pages || []; + this.assets = assets || []; + + this._getTitlecard = memoize(() => + get(find(this.files, { name: 'titlecard', dir: this.base }), [ 0, 'url' ]), + ); + + this._getWebReady = memoize(() => assets && keyBy(assets.map((a) => a.webready()), 'name')); + + this.for = memoize(this.for); + } + + get all () { + return this.files; + } + + get titlecard () { + return this._getTitlecard(); + } + + get webready () { + return this._getWebReady(); + } + + get tasks () { + return this.files.map((a) => a.tasks()).flat(1); + } + + for (dir) { + dir = path.join(this.base, dir); + const subset = filter(this.files, { dir }); + return new this.constructor(subset, dir); + } + + _kindMap () { + return { + [KIND.PAGE]: Page, + [KIND.ASSET]: Asset, + [KIND.OTHER]: File, + }; + } + + _parsePath (filepath) { + if (typeof filepath === 'object') return filepath; + const k = kind(filepath); + const F = this.KIND_MAP[k]; + const f = new F(filepath); + if (f.kind === KIND.PAGE && f.preprocessed) return false; + return f; + } +}; diff --git a/build/index.js b/build/index.js index f8a7ae9..965d72f 100644 --- a/build/index.js +++ b/build/index.js @@ -1,5 +1,8 @@ +process.env.BLUEBIRD_DEBUG = true; + const loadPublicFiles = require('./public'); +const loadPostFiles = require('./posts'); const Cache = require('./cache'); const Promise = require('bluebird'); const fs = require('fs-extra'); @@ -16,21 +19,31 @@ const scripts = require('./scripts'); exports.everything = function (prod = false) { - const fn = async () => { + async function fn () { - // load a directory scan of the public folder - const PublicFiles = await loadPublicFiles(); + // load a directory scan of the public and post folders + const [ PublicFiles, PostFiles ] = await Promise.all([ + loadPublicFiles(), + loadPostFiles(), + ]); // load data for all the files in that folder await Promise.map(PublicFiles.assets, (p) => p.load()); await Promise.map(PublicFiles.pages, (p) => p.load(PublicFiles)); + await Promise.map(PostFiles.assets, (p) => p.load()); + await Promise.map(PostFiles.pages, (p) => p.load(PostFiles)); + + // prime tweet data for all pages const pages = await primeTweets(PublicFiles.pages); + const posts = await primeTweets(PostFiles.pages); + // compile all tasks to be completed const tasks = await Promise.all([ PublicFiles.tasks, + PostFiles.tasks, scss(prod), scripts(prod), svg(prod), @@ -38,6 +51,7 @@ exports.everything = function (prod = false) { ]); await fs.writeFile(resolve('pages.json'), JSON.stringify(pages.map((p) => p.toJson()), null, 2)); + await fs.writeFile(resolve('posts.json'), JSON.stringify(posts.map((p) => p.toJson()), null, 2)); await fs.ensureDir(resolve('dist')); const cache = new Cache({ prod }); @@ -45,10 +59,9 @@ exports.everything = function (prod = false) { await evaluate(tasks.flat(), cache); await cache.save(); - await pageWriter(pages, prod); - }; + await pageWriter([ ...pages, ...posts ], prod); + } - const ret = () => fn().catch((err) => { console.log(err.trace || err); throw err; }); - ret.displayName = prod ? 'generateEverythingForProd' : 'generateEverything'; - return ret; + fn.displayName = prod ? 'buildForProd' : 'build'; + return fn; }; diff --git a/build/page-writer.js b/build/page-writer.js index 748d84d..e7e8cb7 100644 --- a/build/page-writer.js +++ b/build/page-writer.js @@ -44,7 +44,7 @@ module.exports = exports = async function writePageContent (pages, prod) { preview: page.engine === 'md' && String(engines.preview(data.source, data)), }; - const output = resolve('dist', page.output); + const output = resolve('dist', page.out); await fs.ensureDir(path.dirname(output)); await Promise.all([ fs.writeFile(output, Buffer.from(html)), diff --git a/build/page.js b/build/page.js index 79cc243..25e72bc 100644 --- a/build/page.js +++ b/build/page.js @@ -31,23 +31,27 @@ module.exports = exports = class Page extends File { 'flags', ); + this.engine = ENGINE[this.type] || ENGINE.COPY; + } + + _out () { var isIndexPage = (this.name === 'index'); var isClean = isCleanUrl(this.ext); if (isClean && isIndexPage) { - this.output = path.join(this.base, 'index.html'); + this.out = path.join(this.base, 'index.html'); this.json = path.join(this.base, 'index.json'); this.url = this.dir; } else if (isClean) { - this.output = path.join(this.base, this.name, 'index.html'); + this.out = path.join(this.base, this.name, 'index.html'); this.json = path.join(this.base, this.name + '.json'); this.url = path.join(this.dir, this.name); } else if (isIndexPage) { - this.output = path.join(this.base, 'index.html'); + this.out = path.join(this.base, 'index.html'); this.json = path.join(this.base, this.name + '.json'); this.url = this.dir; } else { - this.output = path.join(this.base, this.basename); + this.out = path.join(this.base, this.basename); this.json = path.join(this.base, this.basename + '.json'); this.url = path.join(this.dir, this.basename); } @@ -55,8 +59,6 @@ module.exports = exports = class Page extends File { const url = new URL(pkg.siteInfo.siteUrl); url.pathname = this.url; this.fullurl = url.href; - - this.engine = ENGINE[this.type] || ENGINE.COPY; } async load (PublicFiles) { @@ -65,8 +67,6 @@ module.exports = exports = class Page extends File { fs.stat(this.input).catch(() => ({})), ]); - const { titlecard, assets } = PublicFiles.for(this.dir); - // empty file if (!raw || !ctime) { log.error('Could not load page: ' + this.filepath); @@ -82,20 +82,28 @@ module.exports = exports = class Page extends File { this.source = body; this.meta = meta; - this.images = assets; - this.titlecard = titlecard; - this.tweets = (meta.tweets || []).map(parseTweetId); this.dateCreated = meta.date && new Date(meta.date) || ctime; this.dateModified = mtime; - this.classes = Array.from(new Set(meta.classes || [])); + this._parse(PublicFiles); + + return this; + } + + _parse (PublicFiles) { + const { titlecard, webready } = PublicFiles.for(this.dir); + + this.images = webready; + this.titlecard = titlecard; + this.tweets = (this.meta.tweets || []).map(parseTweetId); + + this.classes = Array.from(new Set(this.meta.classes || [])); this.flags = this.classes.reduce((res, item) => { var camelCased = item.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); res[camelCased] = true; return res; }, {}); - return this; } tasks () { diff --git a/build/post-asset.js b/build/post-asset.js new file mode 100644 index 0000000..5a2a278 --- /dev/null +++ b/build/post-asset.js @@ -0,0 +1,16 @@ + +const { without } = require('lodash'); +const Asset = require('./asset'); + +const postmatch = /(\d{4}-\d\d-\d\d)\.\d{4}\.(\w+)/; + +module.exports = exports = class PostAsset extends Asset { + + _dir (dir) { + dir = dir.replace(postmatch, '$2').split('/'); + dir = without(dir, 'posts', '_images'); + dir.unshift('p'); + return dir; + } + +}; diff --git a/build/post.js b/build/post.js new file mode 100644 index 0000000..eb3d75c --- /dev/null +++ b/build/post.js @@ -0,0 +1,54 @@ + +const path = require('path'); +const { without } = require('lodash'); +const { resolve, isCleanUrl } = require('./resolve'); +const Page = require('./page'); +const pkg = require(resolve('package.json')); + +const postmatch = /(\d{4}-\d\d-\d\d)\.\d{4}\.(\w+)/; + +module.exports = exports = class Post extends Page { + + _dir (dir) { + // if the file name matches the postmatch pattern, then this needs to be /p/ file + const match = this.name.match(postmatch); + + if (match) { + return [ 'p', match[2] ]; + } + + dir = dir.replace(postmatch, '$2').split('/'); + dir = without(dir, 'posts', '_images'); + dir.unshift('p'); + return dir; + } + + _out () { + var isIndexPage = (this.name === 'index' || this.name.match(postmatch)); + var isClean = isCleanUrl(this.ext); + + if (isClean && isIndexPage) { + this.out = path.join(this.base, 'index.html'); + this.json = path.join(this.base, 'index.json'); + this.url = this.dir; + } else if (isClean) { + this.out = path.join(this.base, this.name, 'index.html'); + this.json = path.join(this.base, this.name + '.json'); + this.url = path.join(this.dir, this.name); + } else if (isIndexPage) { + this.out = path.join(this.base, 'index.html'); + this.json = path.join(this.base, this.name + '.json'); + this.url = this.dir; + } else { + this.out = path.join(this.base, this.basename); + this.json = path.join(this.base, this.basename + '.json'); + this.url = path.join(this.dir, this.basename); + } + + const url = new URL(pkg.siteInfo.siteUrl); + url.pathname = this.url; + this.fullurl = url.href; + } + +}; + diff --git a/build/posts.js b/build/posts.js new file mode 100644 index 0000000..63b8d21 --- /dev/null +++ b/build/posts.js @@ -0,0 +1,21 @@ +const glob = require('./lib/glob'); +const { ROOT, KIND } = require('./resolve'); +const File = require('./file'); +const Asset = require('./post-asset'); +const Post = require('./post'); +const Files = require('./files'); + +class PostFiles extends Files { + _kindMap () { + return { + [KIND.PAGE]: Post, + [KIND.ASSET]: Asset, + [KIND.OTHER]: File, + }; + } +} + +module.exports = exports = async function loadPublicFiles () { + return new PostFiles(await glob('posts/**/*', { cwd: ROOT, nodir: true })); +}; + diff --git a/build/public.js b/build/public.js index 2be0ac8..592bf3b 100644 --- a/build/public.js +++ b/build/public.js @@ -1,64 +1,8 @@ const glob = require('./lib/glob'); -const { groupBy, keyBy, filter, find, get, memoize } = require('lodash'); -const { ROOT, kind, KIND } = require('./resolve'); -const File = require('./file'); -const Asset = require('./asset'); -const Page = require('./page'); -const Promise = require('bluebird'); +const { ROOT } = require('./resolve'); -const KIND_MAP = { - [KIND.PAGE]: Page, - [KIND.ASSET]: Asset, - [KIND.OTHER]: File, -}; +const Files = require('./files'); module.exports = exports = async function loadPublicFiles () { - const files = await Promise.map(glob('public/**/*', { cwd: ROOT, nodir: true }), (filepath) => { - const k = kind(filepath); - const F = KIND_MAP[k]; - const f = new F(filepath); - if (f.kind === KIND.PAGE && f.preprocessed) return false; - return f; - }).filter(Boolean); - - const { - [KIND.PAGE]: pages, - [KIND.ASSET]: assets, - } = groupBy(files, 'kind'); - - function within (dir) { - const subset = filter(files, { dir }); - - const getTitlecard = memoize(() => - get(find(files, { name: 'titlecard' }), [ 0, 'url' ]), - ); - - const { - [KIND.PAGE]: subpages, - [KIND.ASSET]: subassets, - } = groupBy(subset, 'kind'); - - const webready = subassets && keyBy(subassets.map((a) => a.webready()), 'name'); - - return { - all: subset, - get titlecard () { return getTitlecard; }, - get pages () { - return subpages; - }, - get assets () { - return webready; - }, - }; - } - - return { - all: files, - pages, - assets, - for: memoize(within), - get tasks () { - return files.map((a) => a.tasks()).flat(1); - }, - }; + return new Files(await glob('public/**/*', { cwd: ROOT, nodir: true })); };