diff --git a/.gitignore b/.gitignore index b4ad9e8..dae330e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,6 @@ coverage .tap # Generated types + *.d.ts *.d.ts.map -!/lib/**/*-types.d.ts -!/index.d.ts diff --git a/README.md b/README.md index f4a8aeb..48b5558 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ siteup (v0.0.11) Siteup builds a website from "pages" in a `src` directory, 1:1 into a `dest` directory. A `src` directory tree might look something like this: -```console +```bash src % tree . ├── a-page diff --git a/index.js b/index.js index e69c295..6628828 100644 --- a/index.js +++ b/index.js @@ -22,21 +22,31 @@ const ignore = ignoreExport.default /** * @template T extends Object. - * @typedef {import('./lib/build-pages/page-builders/template-builder.js').TemplateFunction} TemplateFunction + * @typedef {import('./lib/build-pages/resolve-layout.js').LayoutFunction} LayoutFunction */ /** * @template T extends Object. - * @typedef {import('./lib/build-pages/page-builders/template-builder.js').TemplateAsyncIterator} TemplateAsyncIterator + * @typedef {import('./lib/build-pages/resolve-vars.js').PostVarsFunction} PostVarsFunction */ /** - * @typedef {import('./lib/build-pages/page-builders/template-builder.js').TemplateOutputOverride} TemplateOutputOverride + * @template T extends Object. + * @typedef {import('./lib/build-pages/page-builders/page-writer.js').PageFunction} PageFunction */ /** * @template T extends Object. - * @typedef {import('./lib/build-pages/resolve-layout.js').LayoutFunction} LayoutFunction + * @typedef {import('./lib/build-pages/page-builders/template-builder.js').TemplateFunction} TemplateFunction + */ + +/** + * @template T extends Object. + * @typedef {import('./lib/build-pages/page-builders/template-builder.js').TemplateAsyncIterator} TemplateAsyncIterator + */ + +/** + * @typedef {import('./lib/build-pages/page-builders/template-builder.js').TemplateOutputOverride} TemplateOutputOverride */ export class Siteup { @@ -91,16 +101,16 @@ export class Siteup { this.#cpxWatcher = cpx.watch(getCopyGlob(this.#src), this.#dest, { ignore: this.opts.ignore }) - this.#cpxWatcher.on('copy', (/** @type{{ srcPath: string, dstPath: string }} */e) => { - console.log(`Copy ${e.srcPath} to ${e.dstPath}`) - }) - - this.#cpxWatcher.on('remove', (/** @type{{ path: string }} */e) => { - console.log(`Remove ${e.path}`) - }) - this.#cpxWatcher.on('watch-ready', () => { console.log('Copy watcher ready') + + this.#cpxWatcher.on('copy', (/** @type{{ srcPath: string, dstPath: string }} */e) => { + console.log(`Copy ${e.srcPath} to ${e.dstPath}`) + }) + + this.#cpxWatcher.on('remove', (/** @type{{ path: string }} */e) => { + console.log(`Remove ${e.path}`) + }) }) this.#cpxWatcher.on('watch-error', (/** @type{Error} */err) => { diff --git a/lib/build-esbuild/index.js b/lib/build-esbuild/index.js index f342d04..9b45286 100644 --- a/lib/build-esbuild/index.js +++ b/lib/build-esbuild/index.js @@ -1,6 +1,10 @@ import { join, relative, basename } from 'path' import esbuild from 'esbuild' import { resolveVars } from '../build-pages/resolve-vars.js' +import desm from 'desm' + +const __dirname = desm(import.meta.url) +const SITEUP_DEFAULTS_PREFIX = 'siteup-defaults' /** * @typedef {import('esbuild').Format} EsbuildFormat @@ -31,6 +35,12 @@ export async function buildEsbuild (src, dest, siteData, _opts) { const entryPoints = [] if (siteData.globalClient) entryPoints.push(join(src, siteData.globalClient.relname)) if (siteData.globalStyle) entryPoints.push(join(src, siteData.globalStyle.relname)) + if (siteData.defaultLayout) { + entryPoints.push( + { in: join(__dirname, '../defaults/default.style.css'), out: join(SITEUP_DEFAULTS_PREFIX, 'default.style.css') }, + { in: join(__dirname, '../defaults/default.client.js'), out: join(SITEUP_DEFAULTS_PREFIX, 'default.client.js') } + ) + } for (const page of siteData.pages) { if (page.clientBundle) entryPoints.push(join(src, page.clientBundle.relname)) @@ -42,7 +52,10 @@ export async function buildEsbuild (src, dest, siteData, _opts) { if (layout.layoutStyle) entryPoints.push(join(src, layout.layoutStyle.relname)) } - const browserVars = await resolveVars(siteData?.globalVars?.filepath, 'browser') + const browserVars = await resolveVars({ + varsPath: siteData?.globalVars?.filepath, + key: 'browser' + }) /** @type {{ * [varName: string]: any @@ -83,6 +96,7 @@ export async function buildEsbuild (src, dest, siteData, _opts) { } try { + // @ts-ignore This actually works fine const buildResults = await esbuild.build(buildOpts) /** @type {OutputMap} */ @@ -146,6 +160,14 @@ export async function buildEsbuild (src, dest, siteData, _opts) { } } } + + if (siteData.defaultLayout) { + const defaultClient = Object.values(outputMap).find(p => /^siteup-defaults.*\.js$/.test(p)) + const defaultStyle = Object.values(outputMap).find(p => /^siteup-defaults.*\.css$/.test(p)) + siteData.defaultClient = defaultClient ?? null + siteData.defaultStyle = defaultStyle ?? null + } + return { type: 'esbuild', errors: buildResults.errors, @@ -153,6 +175,7 @@ export async function buildEsbuild (src, dest, siteData, _opts) { report: { buildResults, outputMap, + // @ts-ignore This is fine buildOpts } } diff --git a/lib/build-pages/index.js b/lib/build-pages/index.js index 5e289cb..4895964 100644 --- a/lib/build-pages/index.js +++ b/lib/build-pages/index.js @@ -2,14 +2,17 @@ import { Worker } from 'worker_threads' import desm from 'desm' import { join } from 'path' import pMap from 'p-map' -import { keyBy } from '../helpers/key-by.js' +import { cpus } from 'os' +import { keyBy } from '../helpers/key-by.js' import { resolveVars } from './resolve-vars.js' import { resolveLayout } from './resolve-layout.js' import { pageBuilders, templateBuilder } from './page-builders/index.js' import { PageData } from './page-data.js' import { pageWriter } from './page-builders/page-writer.js' +const MAX_CONCURRENCY = Math.min(cpus().length, 24) + const __dirname = desm(import.meta.url) /** @@ -115,8 +118,12 @@ export async function buildPagesDirect (src, dest, siteData, _opts) { defaultVars, bareGlobalVars ] = await Promise.all([ - resolveVars(join(__dirname, '../defaults/default.vars.js')), - resolveVars(siteData?.globalVars?.filepath) + resolveVars({ + varsPath: join(__dirname, '../defaults/default.vars.js') + }), + resolveVars({ + varsPath: siteData?.globalVars?.filepath + }) ]) /** @type {ResolvedLayout[]} */ @@ -128,16 +135,15 @@ export async function buildPagesDirect (src, dest, siteData, _opts) { layoutStylePath: layout.layoutStyle ? `/${layout.layoutStyle.outputRelname}` : null, layoutClientPath: layout.layoutClient ? `/${layout.layoutClient.outputRelname}` : null } - }, { concurrency: 5 }) + }, { concurrency: MAX_CONCURRENCY }) const resolvedLayouts = keyBy(resolvedLayoutResults, 'name') - if (result.errors.length > 0) return result // Return early, these will all fail. - // Default vars is an internal detail, here we create globalVars that the user sees. /** @type {object} */ const globalVars = { ...defaultVars, + ...(siteData.defaultStyle ? { defaultStyle: true } : {}), ...bareGlobalVars } @@ -147,21 +153,31 @@ export async function buildPagesDirect (src, dest, siteData, _opts) { pageInfo, globalVars, globalStyle: siteData?.globalStyle?.outputRelname, - globalClient: siteData?.globalClient?.outputRelname + globalClient: siteData?.globalClient?.outputRelname, + defaultStyle: siteData?.defaultStyle, + defaultClient: siteData?.defaultClient }) try { // Resolves async vars and binds the page to a reference to its layout fn await pageData.init({ layouts: resolvedLayouts }) } catch (err) { - const variableResolveError = new Error('Error resolving page vars', { cause: err }) + if (!(err instanceof Error)) throw new Error('Non-error thrown while resolving vars', { cause: err }) + const variableResolveError = new Error('Error resolving page vars', { cause: { message: err.message, stack: err.stack } }) // I can't put stuff on the error, the worker swallows it for some reason. result.errors.push({ error: variableResolveError, errorData: { page: pageInfo } }) } return pageData - }, { concurrency: 4 }) + }, { concurrency: MAX_CONCURRENCY }) + + if (result.errors.length > 0) return result - for (const page of pages) { - if (page) { + /** @type {[number, number]} Divided concurrency valus */ + const dividedConcurrency = MAX_CONCURRENCY % 2 + ? [((MAX_CONCURRENCY - 1) / 2) + 1, (MAX_CONCURRENCY - 1) / 2] // odd + : [MAX_CONCURRENCY / 2, MAX_CONCURRENCY / 2] // even + + await Promise.all([ + pMap(pages, async (page) => { try { const buildResult = await pageWriter({ src, @@ -176,26 +192,26 @@ export async function buildPagesDirect (src, dest, siteData, _opts) { // I can't put stuff on the error, the worker swallows it for some reason. result.errors.push({ error: buildError, errorData: { page: page.pageInfo } }) } - } - } + }, { concurrency: dividedConcurrency[0] }), + pMap(siteData.templates, async (template) => { + try { + const buildResult = await templateBuilder({ + src, + dest, + globalVars, + template, + pages + }) - for (const template of siteData.templates) { - try { - const buildResult = await templateBuilder({ - src, - dest, - globalVars, - template, - pages - }) - - result.report.templates.push(buildResult) - } catch (err) { - const buildError = new Error('Error building template', { cause: err }) - // I can't put stuff on the error, the worker swallows it for some reason. - result.errors.push({ error: buildError, errorData: { template } }) - } - } + result.report.templates.push(buildResult) + } catch (err) { + if (!(err instanceof Error)) throw new Error('Non-error thrown while building pages', { cause: err }) + const buildError = new Error('Error building template', { cause: { message: err.message, stack: err.stack } }) + // I can't put stuff on the error, the worker swallows it for some reason. + result.errors.push({ error: buildError, errorData: { template } }) + } + }, { concurrency: dividedConcurrency[1] }) + ]) return result } diff --git a/lib/build-pages/page-builders/html/index.js b/lib/build-pages/page-builders/html/index.js index 33da937..c59a801 100644 --- a/lib/build-pages/page-builders/html/index.js +++ b/lib/build-pages/page-builders/html/index.js @@ -1,5 +1,6 @@ import assert from 'node:assert' import { readFile } from 'fs/promises' +import Handlebars from 'handlebars' /** * @template T @@ -18,6 +19,9 @@ export async function htmlBuilder ({ pageInfo }) { return { vars: {}, - pageLayout: async (/* vars */) => fileContents + pageLayout: async (vars) => { + const template = Handlebars.compile(fileContents) + return template(vars) + } } } diff --git a/lib/build-pages/page-builders/md/get-md.js b/lib/build-pages/page-builders/md/get-md.js new file mode 100644 index 0000000..863274d --- /dev/null +++ b/lib/build-pages/page-builders/md/get-md.js @@ -0,0 +1,87 @@ +import markdownIt from 'markdown-it' +import markdownItFootnote from 'markdown-it-footnote' +import markdownItEmoji from 'markdown-it-emoji' +import markdownItHighlightjs from 'markdown-it-highlightjs' +// @ts-ignore +import markdownItSub from 'markdown-it-sub' +// @ts-ignore +import markdownItSup from 'markdown-it-sup' +// @ts-ignore +import markdownItDeflist from 'markdown-it-deflist' +// @ts-ignore +import markdownItIns from 'markdown-it-ins' +// @ts-ignore +import markdownItMark from 'markdown-it-mark' +// @ts-ignore +import markdownItAbbr from 'markdown-it-abbr' +import Handlebars from 'handlebars' + +const mdOpts = { + html: true, + linkify: true, + typographer: true +} + +export function getMd () { + const md = markdownIt(mdOpts) + .use(markdownItSub) + .use(markdownItSup) + .use(markdownItFootnote) + .use(markdownItDeflist) + .use(markdownItEmoji) + .use(markdownItIns) + .use(markdownItMark) + .use(markdownItAbbr) + // @ts-ignore These @types suck! This works fine. + .use(markdownItHighlightjs, { auto: false, code: true }) + + // disable autolinking for filenames + md.linkify.tlds('.md', false) // markdown + return md +} + +/** + * Renders markdown, and accepts an optional markdown-it instance + * @param {string} mdUnparsed unparsed markdown + * @param {object} vars to expose to handlebars + * @param {markdownIt} [md] an instance of markdown + * @return {string} Rendered markdown to html + */ +export function renderMd (mdUnparsed, vars, md) { + if (!md) md = getMd() + const template = Handlebars.compile(mdUnparsed) + const body = rewriteLinks(md.render(template(vars))) + return body +} + +/** + * Rewrites relative `$1.md` and `$1.markdown` links in body to `$1/index.html`. + * If pretty is false, rewrites `$1.md` to `$1.html`. + * `readme.md` is always rewritten to `index.html`. From sitedown + * + * @param {String} body - html content to rewrite + * @return {String} + */ +function rewriteLinks (body /*, pretty */) { + body = body || '' + + // if (pretty !== false) pretty = true // default to true if omitted + + const regex = /(href=")((?!http[s]*:).*)(\.md|\.markdown)"/g + + return body.replace(regex, function (_match, p1, p2, _p3) { + const f = p2.toLowerCase() + + // root readme + if (f === 'readme') return p1 + '/"' + + // nested readme + if (f.match(/readme$/)) return p1 + f.replace(/readme$/, '') + '"' + + // pretty url + // if (pretty) return p1 + f + '/"' + + // default + return p1 + p2 + '.html"' + }) +} diff --git a/lib/build-pages/page-builders/md/index.js b/lib/build-pages/page-builders/md/index.js index b7f2d25..5b120d5 100644 --- a/lib/build-pages/page-builders/md/index.js +++ b/lib/build-pages/page-builders/md/index.js @@ -2,22 +2,10 @@ import assert from 'node:assert' import { readFile } from 'fs/promises' import yaml from 'js-yaml' import cheerio from 'cheerio' -import markdownIt from 'markdown-it' -import markdownItFootnote from 'markdown-it-footnote' -import markdownItEmoji from 'markdown-it-emoji' -import markdownItHighlightjs from 'markdown-it-highlightjs' -// @ts-ignore -import markdownItSub from 'markdown-it-sub' -// @ts-ignore -import markdownItSup from 'markdown-it-sup' -// @ts-ignore -import markdownItDeflist from 'markdown-it-deflist' -// @ts-ignore -import markdownItIns from 'markdown-it-ins' -// @ts-ignore -import markdownItMark from 'markdown-it-mark' -// @ts-ignore -import markdownItAbbr from 'markdown-it-abbr' + +import { getMd, renderMd } from './get-md.js' + +const md = getMd() /** * @template T @@ -33,75 +21,25 @@ export async function mdBuilder ({ pageInfo }) { assert(pageInfo.type === 'md', 'md builder requires an "md" page type') const fileContents = await readFile(pageInfo.pageFile.filepath, 'utf8') + /** @type {object} */ let frontMatter + /** @type {string} */ let mdUnparsed if (fileContents.trim().startsWith('---')) { const [/* _ */, frontMatterUnparsed, ...mdParts] = fileContents.split('---') mdUnparsed = mdParts.join('---') + // @ts-ignore frontMatter = yaml.load(frontMatterUnparsed ?? '') } else { frontMatter = {} mdUnparsed = fileContents } - const mdOpts = { - html: true, - linkify: true, - typographer: true - } - - const md = markdownIt(mdOpts) - .use(markdownItSub) - .use(markdownItSup) - .use(markdownItFootnote) - .use(markdownItDeflist) - .use(markdownItEmoji) - .use(markdownItIns) - .use(markdownItMark) - .use(markdownItAbbr) - // @ts-ignore These @types suck! This works fine. - .use(markdownItHighlightjs, { auto: false, code: true }) - - // disable autolinking for filenames - md.linkify.tlds('.md', false) // markdown - - const body = rewriteLinks(md.render(mdUnparsed)) + const body = renderMd(mdUnparsed, frontMatter, md) const title = cheerio.load(body)('h1').first().text().trim() return { vars: Object.assign({ title }, frontMatter), - pageLayout: async (/* vars */) => body + pageLayout: async (vars) => renderMd(mdUnparsed, vars, md) } } - -/** - * Rewrites relative `$1.md` and `$1.markdown` links in body to `$1/index.html`. - * If pretty is false, rewrites `$1.md` to `$1.html`. - * `readme.md` is always rewritten to `index.html`. From sitedown - * - * @param {String} body - html content to rewrite - * @return {String} - */ -function rewriteLinks (body /*, pretty */) { - body = body || '' - - // if (pretty !== false) pretty = true // default to true if omitted - - const regex = /(href=")((?!http[s]*:).*)(\.md|\.markdown)"/g - - return body.replace(regex, function (_match, p1, p2, _p3) { - const f = p2.toLowerCase() - - // root readme - if (f === 'readme') return p1 + '/"' - - // nested readme - if (f.match(/readme$/)) return p1 + f.replace(/readme$/, '') + '"' - - // pretty url - // if (pretty) return p1 + f + '/"' - - // default - return p1 + p2 + '.html"' - }) -} diff --git a/lib/build-pages/page-builders/page-writer.js b/lib/build-pages/page-builders/page-writer.js index fecccfc..11feba6 100644 --- a/lib/build-pages/page-builders/page-writer.js +++ b/lib/build-pages/page-builders/page-writer.js @@ -15,7 +15,7 @@ import { writeFile, mkdir } from 'fs/promises' * * @async * @template T extends Object. - * @callback PageLayoutFn + * @callback PageFunction * @param {object} params - The parameters for the pageLayout. * @param {T} params.vars - All default, global, layout, page, and builder vars shallow merged. * @param {string[]} [params.scripts] - Array of script URLs to include. @@ -29,7 +29,7 @@ import { writeFile, mkdir } from 'fs/promises' * @template T extends Object. * @typedef PageBuilderResult * @property {object} vars - Any variables resolved by the builder - * @property {PageLayoutFn} pageLayout - The function that returns the rendered page + * @property {PageFunction} pageLayout - The function that returns the rendered page */ /** diff --git a/lib/build-pages/page-data.js b/lib/build-pages/page-data.js index 0237db3..35e3f2c 100644 --- a/lib/build-pages/page-data.js +++ b/lib/build-pages/page-data.js @@ -1,4 +1,4 @@ -import { resolveVars } from './resolve-vars.js' +import { resolveVars, resolvePostVars } from './resolve-vars.js' import { pageBuilders } from './page-builders/index.js' // @ts-ignore import pretty from 'pretty' @@ -13,6 +13,11 @@ import pretty from 'pretty' * @typedef {import('./index.js').ResolvedLayout} ResolvedLayout */ +/** + * @template T + * @typedef {import('./resolve-vars.js').PostVarsFunction} PostVarsFunction + */ + /** * Represents the data for a page. * @template T extends object @@ -22,10 +27,14 @@ export class PageData { /** @type {ResolvedLayout | null | undefined} */ layout /** @type {object} */ globalVars /** @type {object?} */ pageVars = null + /** @type {function?} */ postVars = null /** @type {object?} */ builderVars = null /** @type {string[]} */ styles = [] /** @type {string[]} */ scripts = [] /** @type {boolean} */ #initalized = false + /** @type {T?} */ #renderedPostVars = null + /** @type {string?} */ #defaultStyle = null + /** @type {string?} */ #defaultClient = null /** * Creates an instance of PageData. @@ -35,15 +44,21 @@ export class PageData { * @param {object} options.globalVars - Global variables available to all pages. * @param {string | undefined} options.globalStyle - Global style path. * @param {string | undefined} options.globalClient - Global client-side script path. + * @param {string?} options.defaultStyle - Default style path. + * @param {string?} options.defaultClient - Default client-side script path. */ constructor ({ pageInfo, globalVars, globalStyle, - globalClient + globalClient, + defaultStyle, + defaultClient }) { this.pageInfo = pageInfo this.globalVars = globalVars + this.#defaultStyle = defaultStyle + this.#defaultClient = defaultClient if (globalStyle) { this.styles.push(`/${globalStyle}`) @@ -68,6 +83,28 @@ export class PageData { } } + /** + * @type {PostVarsFunction} + */ + async #renderPostVars ({ vars, styles, scripts, pages, page }) { + if (!this.#initalized) throw new Error('Initialize PageData before accessing renderPostVars') + if (!this.postVars) return this.vars + if (this.#renderedPostVars) return this.#renderedPostVars + + const { globalVars, pageVars, builderVars } = this + + const renderedPostVars = { + ...globalVars, + ...pageVars, + ...(await this.postVars({ vars, styles, scripts, pages, page })), + ...builderVars + } + + this.#renderedPostVars = renderedPostVars + + return renderedPostVars + } + /** * [init description] * @param {object} params - Parameters required to initialize @@ -78,7 +115,14 @@ export class PageData { const { pageInfo, globalVars } = this if (!pageInfo) throw new Error('A page is required to initialize') const { pageVars, type } = pageInfo - this.pageVars = await resolveVars(pageVars?.filepath) + this.pageVars = await resolveVars({ + varsPath: pageVars?.filepath, + resolveVars: globalVars + }) + this.postVars = await resolvePostVars({ + varsPath: pageVars?.filepath + }) + const builder = pageBuilders[type] const { vars: builderVars } = await builder({ pageInfo }) this.builderVars = builderVars @@ -111,6 +155,12 @@ export class PageData { this.scripts.push(`./${pageInfo.clientBundle.outputName}`) } + // disable-eslint-next-line dot-notation + if ('defaultStyle' in finalVars && finalVars.defaultStyle) { + if (this.#defaultClient) this.scripts.unshift(`/${this.#defaultClient}`) + if (this.#defaultStyle) this.styles.unshift(`/${this.#defaultStyle}`) + } + this.#initalized = true } @@ -120,13 +170,14 @@ export class PageData { * @param {PageData[]} params.pages An array of initialized PageDatas. */ async renderInnerPage ({ pages }) { - if (!this.#initalized) throw new Error('Must be initialized before rendering pages') + if (!this.#initalized) throw new Error('Must be initialized before rendering inner pages') const { pageInfo, styles, scripts, vars } = this if (!pageInfo) throw new Error('A page is required to render') const builder = pageBuilders[pageInfo.type] const { pageLayout } = await builder({ pageInfo }) + const renderedPostVars = await this.#renderPostVars({ vars, styles, scripts, pages, page: pageInfo }) // @ts-ignore - return await pageLayout({ vars, styles, scripts, pages, page: pageInfo }) + return await pageLayout({ vars: renderedPostVars, styles, scripts, pages, page: pageInfo }) } /** @@ -135,15 +186,16 @@ export class PageData { * @param {PageData[]} params.pages An array of initialized PageDatas. */ async renderFullPage ({ pages }) { - if (!this.#initalized) throw new Error('Must be initialized before rendering pages') + if (!this.#initalized) throw new Error('Must be initialized before rendering full pages') const { pageInfo, layout, vars, styles, scripts } = this if (!pageInfo) throw new Error('A page is required to render') if (!layout) throw new Error('A layout is required to render') + const renderedPostVars = await this.#renderPostVars({ vars, styles, scripts, pages, page: pageInfo }) const innerPage = await this.renderInnerPage({ pages }) return pretty( await layout.render({ - vars, + vars: renderedPostVars, styles, scripts, page: pageInfo, diff --git a/lib/build-pages/resolve-vars.js b/lib/build-pages/resolve-vars.js index 56f735b..1e2813e 100644 --- a/lib/build-pages/resolve-vars.js +++ b/lib/build-pages/resolve-vars.js @@ -1,11 +1,25 @@ +/** + * @typedef {import('../identify-pages.js').PageInfo} PageInfo + */ + +/** + * @template T + * @typedef {import('./page-data.js').PageData} PageData + */ + /** * Resolve variables by importing them from a specified path. * - * @param {string} [varsPath] - Path to the file containing the variables. - * @param {string} [key='default'] - The key to extract from the imported module. Default: 'default' - * @returns {Promise} - Returns the resolved variables. If the imported variable is a function, it executes and returns its result. Otherwise, it returns the variable directly. + * @param {object} params + * @param {string} [params.varsPath] - Path to the file containing the variables. + * @param {object} [params.resolveVars] - Any variables you want passed to the reolveFunction. + * @param {string} [params.key='default'] - The key to extract from the imported module. Default: 'default' + * @returns {Promise} - Returns the resolved variables. If the imported variable is a function, it executes and returns its result. Otherwise, it returns the variable directly. */ -export async function resolveVars (varsPath, key = 'default') { +export async function resolveVars ({ + varsPath, + key = 'default' +}) { if (!varsPath) return {} const imported = await import(varsPath) @@ -26,6 +40,47 @@ export async function resolveVars (varsPath, key = 'default') { } } +/** + * postVars functions Can be used to generate page vars but access all page data + * + * @async + * @template T extends Object. + * @callback PostVarsFunction + * @param {object} params - The parameters for the pageLayout. + * @param {T} params.vars - All default, global, layout, page, and builder vars shallow merged. + * @param {string[]} [params.scripts] - Array of script URLs to include. + * @param {string[]} [params.styles] - Array of stylesheet URLs to include. + * @param {PageInfo} params.page - Info about the current page + * @param {PageData[]} params.pages - An array of info about every page + * @returns {Promise} The rendered postVars + */ + +/** + * Resolve variables by importing them from a specified path. + * + * @param {object} params + * @param {string} [params.varsPath] - Path to the file containing the variables. + * @returns {Promise} - Returns the resolved variables. If the imported variable is a function, it executes and returns its result. Otherwise, it returns the variable directly. + */ +export async function resolvePostVars ({ + varsPath +}) { + if (!varsPath) return null + + const imported = await import(varsPath) + const maybePostVars = imported.postVars + + if (maybePostVars) { + if (isFunction(maybePostVars)) { + return maybePostVars + } else { + throw new Error('postVars must export a function') + } + } else { + return null + } +} + /** * Checks if the given value is an object. * diff --git a/lib/build-pages/resolve-vars.test.js b/lib/build-pages/resolve-vars.test.js index 8aa4f62..70ef1a6 100644 --- a/lib/build-pages/resolve-vars.test.js +++ b/lib/build-pages/resolve-vars.test.js @@ -7,9 +7,9 @@ import { resolveVars } from './resolve-vars.js' const __dirname = desm(import.meta.url) tap.test('resolve vars resolves vars', async (t) => { - const varsFile = resolve(__dirname, '../../test-cases/general-features/src/global.vars.js') + const varsPath = resolve(__dirname, '../../test-cases/general-features/src/global.vars.js') - const vars = await resolveVars(varsFile) + const vars = await resolveVars({ varsPath }) // @ts-ignore t.equal(vars.foo, 'global') diff --git a/lib/defaults/default.client.js b/lib/defaults/default.client.js new file mode 100644 index 0000000..4000282 --- /dev/null +++ b/lib/defaults/default.client.js @@ -0,0 +1,4 @@ +// @ts-ignore +import { toggleTheme } from 'mine.css' +// @ts-ignore +window.toggleTheme = toggleTheme diff --git a/lib/defaults/default.root.layout.js b/lib/defaults/default.root.layout.js index 6506ba1..314b2fb 100644 --- a/lib/defaults/default.root.layout.js +++ b/lib/defaults/default.root.layout.js @@ -18,8 +18,8 @@ import { html, render } from 'uhtml-isomorphic' export default function defaultRootLayout ({ vars: { title, - siteName = 'Siteup', - defaultStyle = true + siteName = 'Siteup' + /* defaultStyle = true Set this to false in global or pageto disable the default style in the default layout */ }, scripts, styles, @@ -40,17 +40,6 @@ export default function defaultRootLayout ({ ${styles ? styles.map(style => html``) : null} - ${defaultStyle - ? html` - - - - - ` - : null}
diff --git a/lib/defaults/default.style.css b/lib/defaults/default.style.css new file mode 100644 index 0000000..84d5e65 --- /dev/null +++ b/lib/defaults/default.style.css @@ -0,0 +1,3 @@ +@import 'mine.css/dist/mine.css'; +@import 'mine.css/dist/layout.css'; +@import 'highlight.js/styles/github-dark-dimmed.css'; diff --git a/lib/identify-pages.js b/lib/identify-pages.js index d5a7027..785cf07 100644 --- a/lib/identify-pages.js +++ b/lib/identify-pages.js @@ -271,12 +271,14 @@ export async function identifyPages (src, opts = {}) { // Inject global lauouts and vars const rootFiles = dirs[''] ?? {} + let defaultLayout = false // eslint-disable-next-line dot-notation if (!layouts['root']) { warnings.push({ code: 'SITEUP_WARNING_NO_ROOT_LAYOUT', message: 'Missing a root.layout.js file. Using default layout file.' }) + defaultLayout = true const defaultLayoutBasename = 'default.root.layout.js' const defaultLayoutFilepath = resolve(__dirname, `./defaults/${defaultLayoutBasename}`) @@ -310,11 +312,16 @@ export async function identifyPages (src, opts = {}) { globalClient: rootFiles['global.client.js'], /** @type {PageFileAsset | undefined } */ globalVars: rootFiles['global.vars.js'], + /** @type {string?} Path to a default style */ + defaultStyle: null, + /** @type {string?} Path to a default client */ + defaultClient: null, layouts, templates, pages, warnings, - errors + errors, + defaultLayout // nonPageFolders // allFiles: dirs } diff --git a/package.json b/package.json index 803c3f1..ccd0290 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "cpx2": "^6.0.0", "desm": "^1.1.0", "esbuild": "^0.19.0", + "handlebars": "^4.7.8", + "highlight.js": "^11.9.0", "ignore": "^5.1.8", "js-yaml": "^4.1.0", "make-array": "^1.0.5", diff --git a/test-cases/default-layout/src/global.css b/test-cases/default-layout/src/global.css new file mode 100644 index 0000000..c81052b --- /dev/null +++ b/test-cases/default-layout/src/global.css @@ -0,0 +1,3 @@ +.global-class { + color: blue; +} diff --git a/test-cases/default-layout/src/no-default-style/README.md b/test-cases/default-layout/src/no-default-style/README.md new file mode 100644 index 0000000..7d063bc --- /dev/null +++ b/test-cases/default-layout/src/no-default-style/README.md @@ -0,0 +1,7 @@ +--- +defaultStyle: false +--- + +# No Default style + +This should have no default style. diff --git a/test-cases/general-features/src/README.md b/test-cases/general-features/src/README.md index a6a97b1..f86c563 100644 --- a/test-cases/general-features/src/README.md +++ b/test-cases/general-features/src/README.md @@ -5,3 +5,4 @@ testVar: frontmatter var This is a README.md in the root of the site. +{{{ vars.blogPostsHtml }}} diff --git a/test-cases/general-features/src/html-page/page.html b/test-cases/general-features/src/html-page/page.html index ab92f4d..6598201 100644 --- a/test-cases/general-features/src/html-page/page.html +++ b/test-cases/general-features/src/html-page/page.html @@ -1,3 +1,5 @@
Straight up html goes in here

Dang its been a while since I wrote that directly

+ +

You can use handlebars in html too: {{ vars.someHtmlvars }}

diff --git a/test-cases/general-features/src/page.vars.js b/test-cases/general-features/src/page.vars.js index 53ebe62..ffc2631 100644 --- a/test-cases/general-features/src/page.vars.js +++ b/test-cases/general-features/src/page.vars.js @@ -1,3 +1,52 @@ +// @ts-ignore +import { html, render } from 'uhtml-isomorphic' + +/** + * @template T extends Object. + * @typedef {import('../../../index.js').PostVarsFunction} PostVarsFunction + */ + export default () => ({ testVar: 'page.vars' }) + +/** + * @type {PostVarsFunction<{ + * layout?: string, + * title?: string, + * publishDate?: string + * blogPostsHtml: string + * }>} + */ +export async function postVars ({ + pages +}) { + const blogPosts = pages + .filter(page => page.vars.layout === 'blog' && page.vars.publishDate) + // @ts-ignore + .sort((a, b) => new Date(b.vars.publishDate) - new Date(a.vars.publishDate)) + .slice(0, 5) + + /** @type {string} */ + const blogPostsHtml = render(String, html`
    + ${blogPosts.map(p => { + const publishDate = p.vars.publishDate ? new Date(p.vars.publishDate) : null + return html` +
  • + ${p.vars.title} + ${publishDate + ? html`` + : null + } +
  • +` +})} +
+`) + + return { + blogPostsHtml + } +}