From 3fb98fcbeab5173acec5222bec7a4adf597e88bb Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 7 Jan 2023 09:53:29 -0400 Subject: extract utility binds, content transform fns towards basic dynamics pt. 1 (#124) --- src/write/bind-utilities.js | 257 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 src/write/bind-utilities.js (limited to 'src/write') diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js new file mode 100644 index 00000000..6632ba30 --- /dev/null +++ b/src/write/bind-utilities.js @@ -0,0 +1,257 @@ +// Ties lots and lots of functions together in a convenient package accessible +// to page write functions. This is kept in a separate file from other write +// areas to keep imports neat and isolated. + +import chroma from 'chroma-js'; + +import { + fancifyFlashURL, + fancifyURL, + getAlbumGridHTML, + getAlbumStylesheet, + getArtistString, + getCarouselHTML, + getFlashGridHTML, + getGridHTML, + getRevealStringFromTags, + getRevealStringFromWarnings, + getThemeString, + generateAdditionalFilesList, + generateAdditionalFilesShortcut, + generateChronologyLinks, + generateCoverLink, + generateInfoGalleryLinks, + generateTrackListDividedByGroups, + generateNavigationLinks, + generateStickyHeadingContainer, + iconifyURL, + img, +} from '../misc-templates.js'; + +import { + replacerSpec, + transformInline, + transformLyrics, + transformMultiline, +} from '../util/transform-content.js'; + +import * as html from '../util/html.js'; + +import {bindOpts, withEntries} from '../util/sugar.js'; +import {getColors} from '../util/colors.js'; +import {bindFind} from '../util/find.js'; + +import link, {getLinkThemeString} from '../util/link.js'; + +import { + getAlbumCover, + getArtistAvatar, + getFlashCover, + getTrackCover, +} from '../util/wiki-data.js'; + +export function bindUtilities({ + language, + to, + wikiData, +}) { + // TODO: Is there some nicer way to define these, + // may8e without totally re-8inding everything for + // each page? + const bound = {}; + + bound.html = html; + + bound.img = bindOpts(img, { + [bindOpts.bindIndex]: 0, + html, + }); + + bound.getColors = bindOpts(getColors, { + chroma, + }); + + bound.getLinkThemeString = bindOpts(getLinkThemeString, { + getColors: bound.getColors, + }); + + bound.getThemeString = bindOpts(getThemeString, { + getColors: bound.getColors, + }); + + bound.link = withEntries(link, (entries) => + entries + .map(([key, fn]) => [key, bindOpts(fn, { + getLinkThemeString: bound.getLinkThemeString, + to, + })])); + + bound.find = bindFind(wikiData, {mode: 'warn'}); + + bound.transformInline = bindOpts(transformInline, { + find: bound.find, + link: bound.link, + replacerSpec, + language, + to, + wikiData, + }); + + bound.transformMultiline = bindOpts(transformMultiline, { + img: bound.img, + to, + transformInline: bound.transformInline, + }); + + bound.transformLyrics = bindOpts(transformLyrics, { + transformInline: bound.transformInline, + transformMultiline: bound.transformMultiline, + }); + + bound.iconifyURL = bindOpts(iconifyURL, { + html, + language, + to, + }); + + bound.fancifyURL = bindOpts(fancifyURL, { + html, + language, + }); + + bound.fancifyFlashURL = bindOpts(fancifyFlashURL, { + [bindOpts.bindIndex]: 2, + html, + language, + + fancifyURL: bound.fancifyURL, + }); + + bound.getRevealStringFromWarnings = bindOpts(getRevealStringFromWarnings, { + html, + language, + }); + + bound.getRevealStringFromTags = bindOpts(getRevealStringFromTags, { + language, + + getRevealStringFromWarnings: bound.getRevealStringFromWarnings, + }); + + bound.getArtistString = bindOpts(getArtistString, { + html, + link: bound.link, + language, + + iconifyURL: bound.iconifyURL, + }); + + bound.getAlbumCover = bindOpts(getAlbumCover, { + to, + }); + + bound.getTrackCover = bindOpts(getTrackCover, { + to, + }); + + bound.getFlashCover = bindOpts(getFlashCover, { + to, + }); + + bound.getArtistAvatar = bindOpts(getArtistAvatar, { + to, + }); + + bound.generateAdditionalFilesShortcut = bindOpts(generateAdditionalFilesShortcut, { + html, + language, + }); + + bound.generateAdditionalFilesList = bindOpts(generateAdditionalFilesList, { + html, + language, + }); + + bound.generateNavigationLinks = bindOpts(generateNavigationLinks, { + link: bound.link, + language, + }); + + bound.generateStickyHeadingContainer = bindOpts(generateStickyHeadingContainer, { + [bindOpts.bindIndex]: 0, + getRevealStringFromTags: bound.getRevealStringFromTags, + html, + img: bound.img, + }); + + bound.generateChronologyLinks = bindOpts(generateChronologyLinks, { + html, + language, + link: bound.link, + wikiData, + + generateNavigationLinks: bound.generateNavigationLinks, + }); + + bound.generateCoverLink = bindOpts(generateCoverLink, { + [bindOpts.bindIndex]: 0, + html, + img: bound.img, + link: bound.link, + language, + to, + wikiData, + + getRevealStringFromTags: bound.getRevealStringFromTags, + }); + + bound.generateInfoGalleryLinks = bindOpts(generateInfoGalleryLinks, { + [bindOpts.bindIndex]: 2, + link: bound.link, + language, + }); + + bound.generateTrackListDividedByGroups = bindOpts(generateTrackListDividedByGroups, { + html, + language, + wikiData, + }); + + bound.getGridHTML = bindOpts(getGridHTML, { + [bindOpts.bindIndex]: 0, + img: bound.img, + html, + language, + + getRevealStringFromTags: bound.getRevealStringFromTags, + }); + + bound.getAlbumGridHTML = bindOpts(getAlbumGridHTML, { + [bindOpts.bindIndex]: 0, + link: bound.link, + language, + + getAlbumCover: bound.getAlbumCover, + getGridHTML: bound.getGridHTML, + }); + + bound.getFlashGridHTML = bindOpts(getFlashGridHTML, { + [bindOpts.bindIndex]: 0, + link: bound.link, + + getFlashCover: bound.getFlashCover, + getGridHTML: bound.getGridHTML, + }); + + bound.getCarouselHTML = bindOpts(getCarouselHTML, { + [bindOpts.bindIndex]: 0, + img: bound.img, + html, + }) + + bound.getAlbumStylesheet = bindOpts(getAlbumStylesheet, { + to, + }); + + return bound; +} -- cgit 1.3.0-6-gf8a5 From 613a6a6c253219d85211cdecedacbba45f44a1c2 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 7 Jan 2023 20:07:14 -0400 Subject: extract write validation fns towards basic dynamics pt. 1 (#124) --- src/write/validate-writes.js | 134 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/write/validate-writes.js (limited to 'src/write') diff --git a/src/write/validate-writes.js b/src/write/validate-writes.js new file mode 100644 index 00000000..5d61d0e7 --- /dev/null +++ b/src/write/validate-writes.js @@ -0,0 +1,134 @@ +import {logError} from '../util/cli.js'; + +function validateWritePath(path, urlGroup) { + if (!Array.isArray(path)) { + return {error: `Expected array, got ${path}`}; + } + + const {paths} = urlGroup; + + const definedKeys = Object.keys(paths); + const specifiedKey = path[0]; + + if (!definedKeys.includes(specifiedKey)) { + return {error: `Specified key ${specifiedKey} isn't defined`}; + } + + const expectedArgs = paths[specifiedKey].match(/<>/g)?.length ?? 0; + const specifiedArgs = path.length - 1; + + if (specifiedArgs !== expectedArgs) { + return { + error: `Expected ${expectedArgs} arguments, got ${specifiedArgs}`, + }; + } + + return {success: true}; +} + +function validateWriteObject(obj, { + urlSpec, +}) { + if (typeof obj !== 'object') { + return {error: `Expected object, got ${typeof obj}`}; + } + + if (typeof obj.type !== 'string') { + return {error: `Expected type to be string, got ${obj.type}`}; + } + + switch (obj.type) { + case 'legacy': { + if (typeof obj.write !== 'function') { + return {error: `Expected write to be string, got ${obj.write}`}; + } + + break; + } + + case 'page': { + const path = validateWritePath(obj.path, urlSpec.localized); + if (path.error) { + return {error: `Path validation failed: ${path.error}`}; + } + + if (typeof obj.page !== 'function') { + return {error: `Expected page to be function, got ${obj.content}`}; + } + + break; + } + + case 'data': { + const path = validateWritePath(obj.path, urlSpec.data); + if (path.error) { + return {error: `Path validation failed: ${path.error}`}; + } + + if (typeof obj.data !== 'function') { + return {error: `Expected data to be function, got ${obj.data}`}; + } + + break; + } + + case 'redirect': { + const fromPath = validateWritePath(obj.fromPath, urlSpec.localized); + if (fromPath.error) { + return { + error: `Path (fromPath) validation failed: ${fromPath.error}`, + }; + } + + const toPath = validateWritePath(obj.toPath, urlSpec.localized); + if (toPath.error) { + return {error: `Path (toPath) validation failed: ${toPath.error}`}; + } + + if (typeof obj.title !== 'function') { + return {error: `Expected title to be function, got ${obj.title}`}; + } + + break; + } + + default: { + return {error: `Unknown type: ${obj.type}`}; + } + } + + return {success: true}; +} + +export function validateWrites(writes, { + functionName, + urlSpec, +}) { + // Do a quick valid8tion! If one of the writeThingPages functions go + // wrong, this will stall out early and tell us which did. + + if (!Array.isArray(writes)) { + logError`${functionName} didn't return an array!`; + return false; + } + + if (!( + writes.every((obj) => typeof obj === 'object') && + writes.every((obj) => { + const result = validateWriteObject(obj, { + urlSpec, + }); + if (result.error) { + logError`Validating write object failed: ${result.error}`; + return false; + } else { + return true; + } + }) + )) { + logError`${functionName} returned invalid entries!`; + return false; + } + + return true; +} -- cgit 1.3.0-6-gf8a5 From d59ed9687eb8e38aac3ca7dd5b190ba78b3b5556 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 7 Jan 2023 20:18:51 -0400 Subject: extract page templates (generateDocumentJSON, etc) towards basic dynamcis pt. 1 (#124) --- src/write/page-template.js | 608 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 608 insertions(+) create mode 100644 src/write/page-template.js (limited to 'src/write') diff --git a/src/write/page-template.js b/src/write/page-template.js new file mode 100644 index 00000000..61579549 --- /dev/null +++ b/src/write/page-template.js @@ -0,0 +1,608 @@ +import chroma from 'chroma-js'; + +import * as html from '../util/html.js'; +import {logWarn} from '../util/cli.js'; +import {getColors} from '../util/colors.js'; + +import { + getFooterLocalizationLinks, + getRevealStringFromWarnings, + img, +} from '../misc-templates.js'; + +export function generateDocumentHTML(pageInfo, { + buildTime = null, + cachebust = '', + commit = '', + defaultLanguage, + getThemeString, + language, + languages, + localizedPaths, + paths, + oEmbedJSONHref, + to, + transformMultiline, + wikiData, +}) { + const {wikiInfo} = wikiData; + + let { + title = '', + meta = {}, + theme = '', + stylesheet = '', + + showWikiNameInTitle = true, + themeColor = '', + + // missing properties are auto-filled, see below! + body = {}, + banner = {}, + main = {}, + sidebarLeft = {}, + sidebarRight = {}, + nav = {}, + secondaryNav = {}, + footer = {}, + socialEmbed = {}, + } = pageInfo; + + body.style ??= ''; + + theme = theme || getThemeString(wikiInfo.color); + + banner ||= {}; + banner.classes ??= []; + banner.src ??= ''; + banner.position ??= ''; + banner.dimensions ??= [0, 0]; + + main.classes ??= []; + main.content ??= ''; + + sidebarLeft ??= {}; + sidebarRight ??= {}; + + for (const sidebar of [sidebarLeft, sidebarRight]) { + sidebar.classes ??= []; + sidebar.content ??= ''; + sidebar.collapse ??= true; + } + + nav.classes ??= []; + nav.content ??= ''; + nav.bottomRowContent ??= ''; + nav.links ??= []; + nav.linkContainerClasses ??= []; + + secondaryNav ??= {}; + secondaryNav.content ??= ''; + secondaryNav.content ??= ''; + + footer.classes ??= []; + footer.content ??= wikiInfo.footerContent + ? transformMultiline(wikiInfo.footerContent) + : ''; + + const colors = themeColor + ? getColors(themeColor, {chroma}) + : null; + + const canonical = wikiInfo.canonicalBase + ? wikiInfo.canonicalBase + (paths.pathname === '/' ? '' : paths.pathname) + : ''; + + const localizedCanonical = wikiInfo.canonicalBase + ? Object.entries(localizedPaths).map(([code, {pathname}]) => ({ + lang: code, + href: wikiInfo.canonicalBase + (pathname === '/' ? '' : pathname), + })) + : []; + + const collapseSidebars = + sidebarLeft.collapse !== false && sidebarRight.collapse !== false; + + const mainHTML = + main.content && + html.tag('main', + { + id: 'content', + class: main.classes, + }, + main.content); + + const footerHTML = + html.tag('footer', + { + [html.onlyIfContent]: true, + id: 'footer', + class: footer.classes, + }, + [ + html.tag('div', + { + [html.onlyIfContent]: true, + class: 'footer-content', + }, + footer.content), + + getFooterLocalizationLinks(paths.pathname, { + defaultLanguage, + html, + language, + languages, + paths, + to, + }), + ]); + + const generateSidebarHTML = (id, { + content, + multiple, + classes, + collapse = true, + wide = false, + + // 'last' - last or only sidebar box is sticky + // 'column' - entire column, incl. multiple boxes from top, is sticky + // 'none' - sidebar not sticky at all, stays at top of page + stickyMode = 'last', + }) => + content + ? html.tag('div', + { + id, + class: [ + 'sidebar-column', + 'sidebar', + wide && 'wide', + !collapse && 'no-hide', + stickyMode !== 'none' && 'sticky-' + stickyMode, + ...classes, + ], + }, + content) + : multiple + ? html.tag('div', + { + id, + class: [ + 'sidebar-column', + 'sidebar-multiple', + wide && 'wide', + !collapse && 'no-hide', + stickyMode !== 'none' && 'sticky-' + stickyMode, + ], + }, + multiple + .map((infoOrContent) => + (typeof infoOrContent === 'object' && !Array.isArray(infoOrContent)) + ? infoOrContent + : {content: infoOrContent}) + .filter(({content}) => content) + .map(({ + content, + classes: classes2 = [], + }) => + html.tag('div', + { + class: ['sidebar', ...classes, ...classes2], + }, + html.fragment(content)))) + : ''; + + const sidebarLeftHTML = generateSidebarHTML('sidebar-left', sidebarLeft); + const sidebarRightHTML = generateSidebarHTML('sidebar-right', sidebarRight); + + if (nav.simple) { + nav.linkContainerClasses = ['nav-links-hierarchy']; + nav.links = [{toHome: true}, {toCurrentPage: true}]; + } + + const links = (nav.links || []).filter(Boolean); + + const navLinkParts = []; + for (let i = 0; i < links.length; i++) { + let cur = links[i]; + + let {title: linkTitle} = cur; + + if (cur.toHome) { + linkTitle ??= wikiInfo.nameShort; + } else if (cur.toCurrentPage) { + linkTitle ??= title; + } + + let partContent; + + if (typeof cur.html === 'string') { + partContent = cur.html; + } else { + const attributes = { + class: (cur.toCurrentPage || i === links.length - 1) && 'current', + href: cur.toCurrentPage + ? '' + : cur.toHome + ? to('localized.home') + : cur.path + ? to(...cur.path) + : cur.href + ? (() => { + logWarn`Using legacy href format nav link in ${paths.pathname}`; + return cur.href; + })() + : null, + }; + if (attributes.href === null) { + throw new Error( + `Expected some href specifier for link to ${linkTitle} (${JSON.stringify( + cur + )})` + ); + } + partContent = html.tag('a', attributes, linkTitle); + } + + if (!partContent) continue; + + const part = html.tag('span', + {class: cur.divider === false && 'no-divider'}, + partContent); + + navLinkParts.push(part); + } + + const navHTML = html.tag('nav', + { + [html.onlyIfContent]: true, + id: 'header', + class: [ + ...nav.classes, + links.length && 'nav-has-main-links', + nav.content && 'nav-has-content', + nav.bottomRowContent && 'nav-has-bottom-row', + ], + }, + [ + links.length && + html.tag( + 'div', + {class: ['nav-main-links', ...nav.linkContainerClasses]}, + navLinkParts + ), + nav.bottomRowContent && + html.tag('div', {class: 'nav-bottom-row'}, nav.bottomRowContent), + nav.content && html.tag('div', {class: 'nav-content'}, nav.content), + ]); + + const secondaryNavHTML = html.tag('nav', + { + [html.onlyIfContent]: true, + id: 'secondary-nav', + class: secondaryNav.classes, + }, + secondaryNav.content); + + const bannerSrc = banner.src + ? banner.src + : banner.path + ? to(...banner.path) + : null; + + const bannerHTML = + banner.position && + bannerSrc && + html.tag('div', + { + id: 'banner', + class: banner.classes, + }, + html.tag('img', { + src: bannerSrc, + alt: banner.alt, + width: banner.dimensions[0] || 1100, + height: banner.dimensions[1] || 200, + })); + + const layoutHTML = [ + navHTML, + banner.position === 'top' && bannerHTML, + secondaryNavHTML, + html.tag('div', + { + class: [ + 'layout-columns', + !collapseSidebars && 'vertical-when-thin', + (sidebarLeftHTML || sidebarRightHTML) && 'has-one-sidebar', + (sidebarLeftHTML && sidebarRightHTML) && 'has-two-sidebars', + !(sidebarLeftHTML || sidebarRightHTML) && 'has-zero-sidebars', + sidebarLeftHTML && 'has-sidebar-left', + sidebarRightHTML && 'has-sidebar-right', + ], + }, + [ + sidebarLeftHTML, + mainHTML, + sidebarRightHTML, + ]), + banner.position === 'bottom' && bannerHTML, + footerHTML, + ].filter(Boolean).join('\n'); + + const infoCardHTML = html.tag('div', {id: 'info-card-container'}, + html.tag('div', {id: 'info-card-decor'}, + html.tag('div', {id: 'info-card'}, [ + html.tag('div', {class: ['info-card-art-container', 'no-reveal']}, + img({ + html, + class: 'info-card-art', + src: '', + link: true, + square: true, + })), + html.tag('div', {class: ['info-card-art-container', 'reveal']}, + img({ + html, + class: 'info-card-art', + src: '', + link: true, + square: true, + reveal: getRevealStringFromWarnings( + html.tag('span', {class: 'info-card-art-warnings'}), + {html, language}), + })), + html.tag('h1', {class: 'info-card-name'}, + html.tag('a')), + html.tag('p', {class: 'info-card-album'}, + language.$('releaseInfo.from', { + album: html.tag('a'), + })), + html.tag('p', {class: 'info-card-artists'}, + language.$('releaseInfo.by', { + artists: html.tag('span'), + })), + html.tag('p', {class: 'info-card-cover-artists'}, + language.$('releaseInfo.coverArtBy', { + artists: html.tag('span'), + })), + ]))); + + const socialEmbedHTML = [ + socialEmbed.title && + html.tag('meta', {property: 'og:title', content: socialEmbed.title}), + + socialEmbed.description && + html.tag('meta', { + property: 'og:description', + content: socialEmbed.description, + }), + + socialEmbed.image && + html.tag('meta', {property: 'og:image', content: socialEmbed.image}), + + ...html.fragment( + colors && [ + html.tag('meta', { + name: 'theme-color', + content: colors.dark, + media: '(prefers-color-scheme: dark)', + }), + + html.tag('meta', { + name: 'theme-color', + content: colors.light, + media: '(prefers-color-scheme: light)', + }), + + html.tag('meta', { + name: 'theme-color', + content: colors.primary, + }), + ]), + + oEmbedJSONHref && + html.tag('link', { + type: 'application/json+oembed', + href: oEmbedJSONHref, + }), + ].filter(Boolean).join('\n'); + + return `\n` + html.tag('html', + { + lang: language.intlCode, + 'data-language-code': language.code, + 'data-url-key': paths.urlPath[0], + ...Object.fromEntries( + paths.urlPath.slice(1).map((v, i) => [['data-url-value' + i], v]) + ), + 'data-rebase-localized': to('localized.root'), + 'data-rebase-shared': to('shared.root'), + 'data-rebase-media': to('media.root'), + 'data-rebase-data': to('data.root'), + }, + [ + ``, + + html.tag('head', [ + html.tag('title', + showWikiNameInTitle + ? language.formatString('misc.pageTitle.withWikiName', { + title, + wikiName: wikiInfo.nameShort, + }) + : language.formatString('misc.pageTitle', {title})), + + html.tag('meta', {charset: 'utf-8'}), + html.tag('meta', { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }), + + ...( + Object.entries(meta) + .filter(([key, value]) => value) + .map(([key, value]) => html.tag('meta', {[key]: value}))), + + canonical && + html.tag('link', { + rel: 'canonical', + href: canonical, + }), + + ...( + localizedCanonical + .map(({lang, href}) => html.tag('link', { + rel: 'alternate', + hreflang: lang, + href, + }))), + + socialEmbedHTML, + + html.tag('link', { + rel: 'stylesheet', + href: to('shared.staticFile', `site2.css?${cachebust}`), + }), + + html.tag('style', + {[html.onlyIfContent]: true}, + [ + theme, + stylesheet, + ]), + + html.tag('script', { + src: to('shared.staticFile', `lazy-loading.js?${cachebust}`), + }), + ]), + + html.tag('body', + {style: body.style || ''}, + [ + html.tag('div', {id: 'page-container'}, [ + mainHTML && + html.tag('div', {id: 'skippers'}, + [ + ['#content', language.$('misc.skippers.skipToContent')], + sidebarLeftHTML && + [ + '#sidebar-left', + sidebarRightHTML + ? language.$('misc.skippers.skipToSidebar.left') + : language.$('misc.skippers.skipToSidebar'), + ], + sidebarRightHTML && + [ + '#sidebar-right', + sidebarLeftHTML + ? language.$('misc.skippers.skipToSidebar.right') + : language.$('misc.skippers.skipToSidebar'), + ], + footerHTML && + ['#footer', language.$('misc.skippers.skipToFooter')], + ] + .filter(Boolean) + .map(([href, title]) => + html.tag('span', {class: 'skipper'}, + html.tag('a', {href}, title)))), + layoutHTML, + ]), + + infoCardHTML, + + html.tag('script', { + type: 'module', + src: to('shared.staticFile', `client.js?${cachebust}`), + }), + ]), + ]); +} + +export function generateOEmbedJSON(pageInfo, {language, wikiData}) { + const {socialEmbed} = pageInfo; + const {wikiInfo} = wikiData; + const {canonicalBase, nameShort} = wikiInfo; + + if (!socialEmbed) return ''; + + const entries = [ + socialEmbed.heading && [ + 'author_name', + language.$('misc.socialEmbed.heading', { + wikiName: nameShort, + heading: socialEmbed.heading, + }), + ], + socialEmbed.headingLink && + canonicalBase && [ + 'author_url', + canonicalBase.replace(/\/$/, '') + + '/' + + socialEmbed.headingLink.replace(/^\//, ''), + ], + ].filter(Boolean); + + if (!entries.length) return ''; + + return JSON.stringify(Object.fromEntries(entries)); +} + +export function generateRedirectHTML(title, target, { + language, +}) { + return `\n` + html.tag('html', [ + html.tag('head', [ + html.tag('title', language.$('redirectPage.title', {title})), + html.tag('meta', {charset: 'utf-8'}), + + html.tag('meta', { + 'http-equiv': 'refresh', + content: `0;url=${target}`, + }), + + // TODO: Is this OK for localized pages? + html.tag('link', { + rel: 'canonical', + href: target, + }), + ]), + + html.tag('body', + html.tag('main', [ + html.tag('h1', + language.$('redirectPage.title', {title})), + html.tag('p', + language.$('redirectPage.infoLine', { + target: html.tag('a', {href: target}, target), + })), + ])), + ]); +} -- cgit 1.3.0-6-gf8a5 From 078b9656fe16738967943e4ca94866481f4f1d21 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sat, 7 Jan 2023 21:08:08 -0400 Subject: extract actual file IO functions --- src/write/page-template.js | 21 ++++++ src/write/write-files.js | 161 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 src/write/write-files.js (limited to 'src/write') diff --git a/src/write/page-template.js b/src/write/page-template.js index 61579549..f7faeed0 100644 --- a/src/write/page-template.js +++ b/src/write/page-template.js @@ -606,3 +606,24 @@ export function generateRedirectHTML(title, target, { ])), ]); } + +export function generateGlobalWikiDataJSON({ + serializeThings, + wikiData, +}) { + return '{\n' + + ([ + `"albumData": ${stringifyThings(wikiData.albumData)},`, + wikiData.wikiInfo.enableFlashesAndGames && + `"flashData": ${stringifyThings(wikiData.flashData)},`, + `"artistData": ${stringifyThings(wikiData.artistData)}`, + ] + .filter(Boolean) + .map(line => ' ' + line) + .join('\n')) + + '\n}'; + + function stringifyThings(thingData) { + return JSON.stringify(serializeThings(thingData)); + } +} diff --git a/src/write/write-files.js b/src/write/write-files.js new file mode 100644 index 00000000..e448df3f --- /dev/null +++ b/src/write/write-files.js @@ -0,0 +1,161 @@ +import * as path from 'path'; + +import {generateRedirectHTML} from './page-template.js'; + +import { + logInfo, + logWarn, + progressPromiseAll, +} from '../util/cli.js'; + +// Code that's common 8etween the 8uild code (i.e. upd8.js) and gener8ted +// site code should 8e put here. Which, uh, ~~only really means this one +// file~~ is now a variety of useful utilities! +// +// Rather than hard code it, anything in this directory can 8e shared across +// 8oth ends of the code8ase. +// (This gets symlinked into the --data-path directory.) +const UTILITY_DIRECTORY = 'util'; + +// Code that's used only in the static site! CSS, cilent JS, etc. +// (This gets symlinked into the --data-path directory.) +const STATIC_DIRECTORY = 'static'; + +import { + copyFile, + mkdir, + stat, + symlink, + writeFile, + unlink, +} from 'fs/promises'; + +export async function writePage({ + html, + oEmbedJSON = '', + paths, +}) { + await mkdir(paths.output.directory, {recursive: true}); + + await Promise.all([ + writeFile(paths.output.documentHTML, html), + + oEmbedJSON && + writeFile(paths.output.oEmbedJSON, oEmbedJSON), + ].filter(Boolean)); +} + +export function writeSymlinks({ + srcRootDirname, + mediaPath, + outputPath, + urls, +}) { + return progressPromiseAll('Writing site symlinks.', [ + link(path.join(srcRootDirname, UTILITY_DIRECTORY), 'shared.utilityRoot'), + link(path.join(srcRootDirname, STATIC_DIRECTORY), 'shared.staticRoot'), + link(mediaPath, 'media.root'), + ]); + + async function link(directory, urlKey) { + const pathname = urls.from('shared.root').toDevice(urlKey); + const file = path.join(outputPath, pathname); + + try { + await unlink(file); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + try { + await symlink(path.resolve(directory), file); + } catch (error) { + if (error.code === 'EPERM') { + await symlink(path.resolve(directory), file, 'junction'); + } + } + } +} + +export async function writeFavicon({ + mediaPath, + outputPath, +}) { + const faviconFile = 'favicon.ico'; + + try { + await stat(path.join(mediaPath, faviconFile)); + } catch (error) { + return; + } + + try { + await copyFile( + path.join(mediaPath, faviconFile), + path.join(outputPath, faviconFile)); + } catch (error) { + logWarn`Failed to copy favicon! ${error.message}`; + return; + } + + logInfo`Copied favicon to site root.`; +} + +export async function writeSharedFilesAndPages({ + language, + mediaPath, + outputPath, + urls, + wikiData, + wikiDataJSON, +}) { + const {groupData, wikiInfo} = wikiData; + + await writeFavicon({ + mediaPath, + outputPath, + }); + + return progressPromiseAll(`Writing files & pages shared across languages.`, [ + groupData?.some((group) => group.directory === 'fandom') && + redirect( + 'Fandom - Gallery', + 'albums/fandom', + 'localized.groupGallery', + 'fandom' + ), + + groupData?.some((group) => group.directory === 'official') && + redirect( + 'Official - Gallery', + 'albums/official', + 'localized.groupGallery', + 'official' + ), + + wikiInfo.enableListings && + redirect( + 'Album Commentary', + 'list/all-commentary', + 'localized.commentaryIndex', + '' + ), + + wikiDataJSON && + writeFile( + path.join(outputPath, 'data.json'), + wikiDataJSON), + ].filter(Boolean)); + + async function redirect(title, from, urlKey, directory) { + const target = path.relative( + from, + urls.from('shared.root').to(urlKey, directory) + ); + const content = generateRedirectHTML(title, target, {language}); + await mkdir(path.join(outputPath, from), {recursive: true}); + await writeFile(path.join(outputPath, from, 'index.html'), content); + } +} -- cgit 1.3.0-6-gf8a5 From db816222a07a4a8dff9f0f8b66e7aa7cb7c15eb5 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 8 Jan 2023 10:52:12 -0400 Subject: extract static-build, new modular build modes --- src/write/build-modes/index.js | 2 + src/write/build-modes/live-dev-server.js | 31 +++ src/write/build-modes/static-build.js | 423 +++++++++++++++++++++++++++++++ src/write/page-template.js | 70 ++--- src/write/write-files.js | 6 +- 5 files changed, 498 insertions(+), 34 deletions(-) create mode 100644 src/write/build-modes/index.js create mode 100644 src/write/build-modes/live-dev-server.js create mode 100644 src/write/build-modes/static-build.js (limited to 'src/write') diff --git a/src/write/build-modes/index.js b/src/write/build-modes/index.js new file mode 100644 index 00000000..91e39009 --- /dev/null +++ b/src/write/build-modes/index.js @@ -0,0 +1,2 @@ +export * as 'live-dev-server' from './live-dev-server.js'; +export * as 'static-build' from './static-build.js'; diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js new file mode 100644 index 00000000..76b6d47f --- /dev/null +++ b/src/write/build-modes/live-dev-server.js @@ -0,0 +1,31 @@ +import {logInfo} from '../../util/cli.js'; + +export function getCLIOptions() { + // Stub. + return {}; +} + +export async function go({ + _cliOptions, + _dataPath, + _mediaPath, + _queueSize, + + _defaultLanguage, + _languages, + _srcRootPath, + _urls, + _urlSpec, + _wikiData, + + _cachebust, + _developersComment, + _getSizeOfAdditionalFile, +}) { + // Stub. + logInfo`So we are back in the mine!`; + logInfo`We are swinging our pickaxe in-`; + logInfo`...multiple directions,`; + logInfo`...multiple directions.`; + return true; +} diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js new file mode 100644 index 00000000..eafb53d6 --- /dev/null +++ b/src/write/build-modes/static-build.js @@ -0,0 +1,423 @@ +import {bindUtilities} from '../bind-utilities.js'; +import {validateWrites} from '../validate-writes.js'; + +import { + generateDocumentHTML, + generateGlobalWikiDataJSON, + generateOEmbedJSON, + generateRedirectHTML, +} from '../page-template.js'; + +import { + writePage, + writeSharedFilesAndPages, + writeSymlinks, +} from '../write-files.js'; + +import {serializeThings} from '../../data/serialize.js'; + +import * as pageSpecs from '../../page/index.js'; + +import link from '../../util/link.js'; +import {empty, queue, withEntries} from '../../util/sugar.js'; +import {getPagePaths, getURLsFrom} from '../../util/urls.js'; + +import { + logError, + logInfo, + logWarn, + progressCallAll, + progressPromiseAll, +} from '../../util/cli.js'; + +const pageFlags = Object.keys(pageSpecs); + +export function getCLIOptions() { + return { + // This is the output directory. It's the one you'll upload online with + // rsync or whatever when you're pushing an upd8, and also the one + // you'd archive if you wanted to make a 8ackup of the whole dang + // site. Just keep in mind that the gener8ted result will contain a + // couple symlinked directories, so if you're uploading, you're pro8a8ly + // gonna want to resolve those yourself. + 'out-path': { + type: 'value', + }, + + // Working without a dev server and just using file:// URLs in your we8 + // 8rowser? This will automatically append index.html to links across + // the site. Not recommended for production, since it isn't guaranteed + // 100% error-free (and index.html-style links are less pretty anyway). + 'append-index-html': { + type: 'flag', + }, + + // Only want to 8uild one language during testing? This can chop down + // 8uild times a pretty 8ig chunk! Just pass a single language code. + 'lang': { + type: 'value', + }, + + // NOT for neatly ena8ling or disa8ling specific features of the site! + // This is only in charge of what general groups of files to write. + // They're here to make development quicker when you're only working + // on some particular area(s) of the site rather than making changes + // across all of them. + ...Object.fromEntries(pageFlags.map((key) => [key, {type: 'flag'}])), + }; +} + +export async function go({ + cliOptions, + _dataPath, + mediaPath, + queueSize, + + defaultLanguage, + languages, + srcRootPath, + urls, + urlSpec, + wikiData, + + cachebust, + developersComment, + getSizeOfAdditionalFile, +}) { + const outputPath = cliOptions['out-path'] || process.env.HSMUSIC_OUT; + const appendIndexHTML = cliOptions['append-index-html'] ?? false; + const writeOneLanguage = cliOptions['lang'] ?? null; + + if (!outputPath) { + logError`Expected ${'--out-path'} option or ${'HSMUSIC_OUT'} to be set`; + return false; + } + + if (appendIndexHTML) { + logWarn`Appending index.html to link hrefs. (Note: not recommended for production release!)`; + link.globalOptions.appendIndexHTML = true; + } + + if (writeOneLanguage && !(writeOneLanguage in languages)) { + logError`Specified to write only ${writeOneLanguage}, but there is no strings file with this language code!`; + return false; + } else if (writeOneLanguage) { + logInfo`Writing only language ${writeOneLanguage} this run.`; + } else { + logInfo`Writing all languages.`; + } + + const selectedPageFlags = Object.keys(cliOptions) + .filter(key => pageFlags.includes(key)); + + const writeAll = empty(selectedPageFlags) || selectedPageFlags.includes('all'); + logInfo`Writing site pages: ${writeAll ? 'all' : selectedPageFlags.join(', ')}`; + + await writeSymlinks({ + srcRootPath, + mediaPath, + outputPath, + urls, + }); + + await writeSharedFilesAndPages({ + mediaPath, + outputPath, + urls, + + language: defaultLanguage, + wikiData, + wikiDataJSON: generateGlobalWikiDataJSON({ + serializeThings, + wikiData, + }) + }); + + const buildSteps = writeAll + ? Object.entries(pageSpecs) + : Object.entries(pageSpecs) + .filter(([flag]) => selectedPageFlags.includes(flag)); + + let writes; + { + let error = false; + + const buildStepsWithTargets = buildSteps + .map(([flag, pageSpec]) => { + // Condition not met: skip this build step altogether. + if (pageSpec.condition && !pageSpec.condition({wikiData})) { + return null; + } + + // May still call writeTargetless if present. + if (!pageSpec.targets) { + return {flag, pageSpec, targets: []}; + } + + if (!pageSpec.write) { + logError`${flag + '.targets'} is specified, but ${flag + '.write'} is missing!`; + error = true; + return null; + } + + const targets = pageSpec.targets({wikiData}); + if (!Array.isArray(targets)) { + logError`${flag + '.targets'} was called, but it didn't return an array! (${typeof targets})`; + error = true; + return null; + } + + return {flag, pageSpec, targets}; + }) + .filter(Boolean); + + if (error) { + return false; + } + + writes = progressCallAll('Computing page & data writes.', buildStepsWithTargets.flatMap(({flag, pageSpec, targets}) => { + const writesFns = targets.map(target => () => { + const writes = pageSpec.write(target, {wikiData})?.slice() || []; + const valid = validateWrites(writes, { + functionName: flag + '.write', + urlSpec, + }); + error ||=! valid; + return valid ? writes : []; + }); + + if (pageSpec.writeTargetless) { + writesFns.push(() => { + const writes = pageSpec.writeTargetless({wikiData}); + const valid = validateWrites(writes, { + functionName: flag + '.writeTargetless', + urlSpec, + }); + error ||=! valid; + return valid ? writes : []; + }); + } + + return writesFns; + })).flat(); + + if (error) { + return false; + } + } + + const pageWrites = writes.filter(({type}) => type === 'page'); + const dataWrites = writes.filter(({type}) => type === 'data'); + const redirectWrites = writes.filter(({type}) => type === 'redirect'); + + if (writes.length) { + logInfo`Total of ${writes.length} writes returned. (${pageWrites.length} page, ${dataWrites.length} data [currently skipped], ${redirectWrites.length} redirect)`; + } else { + logWarn`No writes returned at all, so exiting early. This is probably a bug!`; + return false; + } + + /* + await progressPromiseAll(`Writing data files shared across languages.`, queue( + dataWrites.map(({path, data}) => () => { + const bound = {}; + + bound.serializeLink = bindOpts(serializeLink, {}); + + bound.serializeContribs = bindOpts(serializeContribs, {}); + + bound.serializeImagePaths = bindOpts(serializeImagePaths, { + thumb + }); + + bound.serializeCover = bindOpts(serializeCover, { + [bindOpts.bindIndex]: 2, + serializeImagePaths: bound.serializeImagePaths, + urls + }); + + bound.serializeGroupsForAlbum = bindOpts(serializeGroupsForAlbum, { + serializeLink + }); + + bound.serializeGroupsForTrack = bindOpts(serializeGroupsForTrack, { + serializeLink + }); + + // TODO: This only supports one <>-style argument. + return writeData(path[0], path[1], data({...bound})); + }), + queueSize + )); + */ + + const perLanguageFn = async (language, i, entries) => { + const baseDirectory = + language === defaultLanguage ? '' : language.code; + + console.log(`\x1b[34;1m${`[${i + 1}/${entries.length}] ${language.code} (-> /${baseDirectory}) `.padEnd(60, '-')}\x1b[0m`); + + await progressPromiseAll(`Writing ${language.code}`, queue([ + ...pageWrites.map((props) => () => { + const {path, page} = props; + + const pageSubKey = path[0]; + const urlArgs = path.slice(1); + + const localizedPaths = withEntries(languages, entries => entries + .filter(([key, language]) => key !== 'default' && !language.hidden) + .map(([_key, language]) => [ + language.code, + getPagePaths({ + outputPath, + urls, + + baseDirectory: + (language === defaultLanguage + ? '' + : language.code), + fullKey: 'localized.' + pageSubKey, + urlArgs, + }), + ])); + + const paths = getPagePaths({ + outputPath, + urls, + + baseDirectory, + fullKey: 'localized.' + pageSubKey, + urlArgs, + }); + + const to = getURLsFrom({ + urls, + baseDirectory, + pageSubKey, + paths, + }); + + const absoluteTo = (targetFullKey, ...args) => { + const [groupKey, subKey] = targetFullKey.split('.'); + const from = urls.from('shared.root'); + return ( + '/' + + (groupKey === 'localized' && baseDirectory + ? from.to( + 'localizedWithBaseDirectory.' + subKey, + baseDirectory, + ...args + ) + : from.to(targetFullKey, ...args)) + ); + }; + + const bound = bindUtilities({ + language, + to, + wikiData, + }); + + const pageInfo = page({ + ...bound, + + language, + + absoluteTo, + relativeTo: to, + to, + urls, + + getSizeOfAdditionalFile, + }); + + const oEmbedJSON = generateOEmbedJSON(pageInfo, { + language, + wikiData, + }); + + const oEmbedJSONHref = + oEmbedJSON && + wikiData.wikiInfo.canonicalBase && + wikiData.wikiInfo.canonicalBase + + urls + .from('shared.root') + .to('shared.path', paths.pathname + 'oembed.json'); + + const pageHTML = generateDocumentHTML(pageInfo, { + cachebust, + defaultLanguage, + developersComment, + getThemeString: bound.getThemeString, + language, + languages, + localizedPaths, + oEmbedJSONHref, + paths, + to, + transformMultiline: bound.transformMultiline, + wikiData, + }); + + return writePage({ + html: pageHTML, + oEmbedJSON, + paths, + }); + }), + ...redirectWrites.map(({fromPath, toPath, title: titleFn}) => () => { + const title = titleFn({ + language, + }); + + const from = getPagePaths({ + outputPath, + urls, + + baseDirectory, + fullKey: 'localized.' + fromPath[0], + urlArgs: fromPath.slice(1), + }); + + const to = getURLsFrom({ + urls, + baseDirectory, + pageSubKey: fromPath[0], + paths: from, + }); + + const target = to('localized.' + toPath[0], ...toPath.slice(1)); + const html = generateRedirectHTML(title, target, {language}); + return writePage({html, paths: from}); + }), + ], queueSize)); + }; + + await wrapLanguages(perLanguageFn, { + languages, + writeOneLanguage, + }); + + // The single most important step. + logInfo`Written!`; + return true; +} + +// Wrapper function for running a function once for all languages. +async function wrapLanguages(fn, { + languages, + writeOneLanguage = null, +}) { + const k = writeOneLanguage; + const languagesToRun = k ? {[k]: languages[k]} : languages; + + const entries = Object.entries(languagesToRun).filter( + ([key]) => key !== 'default' + ); + + for (let i = 0; i < entries.length; i++) { + const [_key, language] = entries[i]; + + await fn(language, i, entries); + } +} diff --git a/src/write/page-template.js b/src/write/page-template.js index f7faeed0..efbc5795 100644 --- a/src/write/page-template.js +++ b/src/write/page-template.js @@ -10,11 +10,46 @@ import { img, } from '../misc-templates.js'; +export function generateDevelopersCommentHTML({ + buildTime, + commit, + wikiData, +}) { + const {name, canonicalBase} = wikiData.wikiInfo; + return ``; +} + export function generateDocumentHTML(pageInfo, { - buildTime = null, - cachebust = '', - commit = '', + cachebust, defaultLanguage, + developersComment, getThemeString, language, languages, @@ -422,34 +457,7 @@ export function generateDocumentHTML(pageInfo, { 'data-rebase-data': to('data.root'), }, [ - ``, + developersComment, html.tag('head', [ html.tag('title', diff --git a/src/write/write-files.js b/src/write/write-files.js index e448df3f..8b6ac3af 100644 --- a/src/write/write-files.js +++ b/src/write/write-files.js @@ -46,14 +46,14 @@ export async function writePage({ } export function writeSymlinks({ - srcRootDirname, + srcRootPath, mediaPath, outputPath, urls, }) { return progressPromiseAll('Writing site symlinks.', [ - link(path.join(srcRootDirname, UTILITY_DIRECTORY), 'shared.utilityRoot'), - link(path.join(srcRootDirname, STATIC_DIRECTORY), 'shared.staticRoot'), + link(path.join(srcRootPath, UTILITY_DIRECTORY), 'shared.utilityRoot'), + link(path.join(srcRootPath, STATIC_DIRECTORY), 'shared.staticRoot'), link(mediaPath, 'media.root'), ]); -- cgit 1.3.0-6-gf8a5 From 2e90eaed378491142cb9d57ce705c58f4a598a10 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 8 Jan 2023 10:59:13 -0400 Subject: move static-specific write fns into build mode --- src/write/build-modes/static-build.js | 148 +++++++++++++++++++++++++++++-- src/write/write-files.js | 161 ---------------------------------- 2 files changed, 140 insertions(+), 169 deletions(-) delete mode 100644 src/write/write-files.js (limited to 'src/write') diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index eafb53d6..b3700c43 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -1,3 +1,5 @@ +import * as path from 'path'; + import {bindUtilities} from '../bind-utilities.js'; import {validateWrites} from '../validate-writes.js'; @@ -8,12 +10,6 @@ import { generateRedirectHTML, } from '../page-template.js'; -import { - writePage, - writeSharedFilesAndPages, - writeSymlinks, -} from '../write-files.js'; - import {serializeThings} from '../../data/serialize.js'; import * as pageSpecs from '../../page/index.js'; @@ -120,12 +116,15 @@ export async function go({ urls, }); - await writeSharedFilesAndPages({ + await writeFavicon({ mediaPath, outputPath, - urls, + }); + await writeSharedFilesAndPages({ language: defaultLanguage, + outputPath, + urls, wikiData, wikiDataJSON: generateGlobalWikiDataJSON({ serializeThings, @@ -421,3 +420,136 @@ async function wrapLanguages(fn, { await fn(language, i, entries); } } + +import { + copyFile, + mkdir, + stat, + symlink, + writeFile, + unlink, +} from 'fs/promises'; + +async function writePage({ + html, + oEmbedJSON = '', + paths, +}) { + await mkdir(paths.output.directory, {recursive: true}); + + await Promise.all([ + writeFile(paths.output.documentHTML, html), + + oEmbedJSON && + writeFile(paths.output.oEmbedJSON, oEmbedJSON), + ].filter(Boolean)); +} + +function writeSymlinks({ + srcRootPath, + mediaPath, + outputPath, + urls, +}) { + return progressPromiseAll('Writing site symlinks.', [ + link(path.join(srcRootPath, 'util'), 'shared.utilityRoot'), + link(path.join(srcRootPath, 'static'), 'shared.staticRoot'), + link(mediaPath, 'media.root'), + ]); + + async function link(directory, urlKey) { + const pathname = urls.from('shared.root').toDevice(urlKey); + const file = path.join(outputPath, pathname); + + try { + await unlink(file); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + try { + await symlink(path.resolve(directory), file); + } catch (error) { + if (error.code === 'EPERM') { + await symlink(path.resolve(directory), file, 'junction'); + } + } + } +} + +async function writeFavicon({ + mediaPath, + outputPath, +}) { + const faviconFile = 'favicon.ico'; + + try { + await stat(path.join(mediaPath, faviconFile)); + } catch (error) { + return; + } + + try { + await copyFile( + path.join(mediaPath, faviconFile), + path.join(outputPath, faviconFile)); + } catch (error) { + logWarn`Failed to copy favicon! ${error.message}`; + return; + } + + logInfo`Copied favicon to site root.`; +} + +async function writeSharedFilesAndPages({ + language, + outputPath, + urls, + wikiData, + wikiDataJSON, +}) { + const {groupData, wikiInfo} = wikiData; + + return progressPromiseAll(`Writing files & pages shared across languages.`, [ + groupData?.some((group) => group.directory === 'fandom') && + redirect( + 'Fandom - Gallery', + 'albums/fandom', + 'localized.groupGallery', + 'fandom' + ), + + groupData?.some((group) => group.directory === 'official') && + redirect( + 'Official - Gallery', + 'albums/official', + 'localized.groupGallery', + 'official' + ), + + wikiInfo.enableListings && + redirect( + 'Album Commentary', + 'list/all-commentary', + 'localized.commentaryIndex', + '' + ), + + wikiDataJSON && + writeFile( + path.join(outputPath, 'data.json'), + wikiDataJSON), + ].filter(Boolean)); + + async function redirect(title, from, urlKey, directory) { + const target = path.relative( + from, + urls.from('shared.root').to(urlKey, directory) + ); + const content = generateRedirectHTML(title, target, {language}); + await mkdir(path.join(outputPath, from), {recursive: true}); + await writeFile(path.join(outputPath, from, 'index.html'), content); + } +} diff --git a/src/write/write-files.js b/src/write/write-files.js deleted file mode 100644 index 8b6ac3af..00000000 --- a/src/write/write-files.js +++ /dev/null @@ -1,161 +0,0 @@ -import * as path from 'path'; - -import {generateRedirectHTML} from './page-template.js'; - -import { - logInfo, - logWarn, - progressPromiseAll, -} from '../util/cli.js'; - -// Code that's common 8etween the 8uild code (i.e. upd8.js) and gener8ted -// site code should 8e put here. Which, uh, ~~only really means this one -// file~~ is now a variety of useful utilities! -// -// Rather than hard code it, anything in this directory can 8e shared across -// 8oth ends of the code8ase. -// (This gets symlinked into the --data-path directory.) -const UTILITY_DIRECTORY = 'util'; - -// Code that's used only in the static site! CSS, cilent JS, etc. -// (This gets symlinked into the --data-path directory.) -const STATIC_DIRECTORY = 'static'; - -import { - copyFile, - mkdir, - stat, - symlink, - writeFile, - unlink, -} from 'fs/promises'; - -export async function writePage({ - html, - oEmbedJSON = '', - paths, -}) { - await mkdir(paths.output.directory, {recursive: true}); - - await Promise.all([ - writeFile(paths.output.documentHTML, html), - - oEmbedJSON && - writeFile(paths.output.oEmbedJSON, oEmbedJSON), - ].filter(Boolean)); -} - -export function writeSymlinks({ - srcRootPath, - mediaPath, - outputPath, - urls, -}) { - return progressPromiseAll('Writing site symlinks.', [ - link(path.join(srcRootPath, UTILITY_DIRECTORY), 'shared.utilityRoot'), - link(path.join(srcRootPath, STATIC_DIRECTORY), 'shared.staticRoot'), - link(mediaPath, 'media.root'), - ]); - - async function link(directory, urlKey) { - const pathname = urls.from('shared.root').toDevice(urlKey); - const file = path.join(outputPath, pathname); - - try { - await unlink(file); - } catch (error) { - if (error.code !== 'ENOENT') { - throw error; - } - } - - try { - await symlink(path.resolve(directory), file); - } catch (error) { - if (error.code === 'EPERM') { - await symlink(path.resolve(directory), file, 'junction'); - } - } - } -} - -export async function writeFavicon({ - mediaPath, - outputPath, -}) { - const faviconFile = 'favicon.ico'; - - try { - await stat(path.join(mediaPath, faviconFile)); - } catch (error) { - return; - } - - try { - await copyFile( - path.join(mediaPath, faviconFile), - path.join(outputPath, faviconFile)); - } catch (error) { - logWarn`Failed to copy favicon! ${error.message}`; - return; - } - - logInfo`Copied favicon to site root.`; -} - -export async function writeSharedFilesAndPages({ - language, - mediaPath, - outputPath, - urls, - wikiData, - wikiDataJSON, -}) { - const {groupData, wikiInfo} = wikiData; - - await writeFavicon({ - mediaPath, - outputPath, - }); - - return progressPromiseAll(`Writing files & pages shared across languages.`, [ - groupData?.some((group) => group.directory === 'fandom') && - redirect( - 'Fandom - Gallery', - 'albums/fandom', - 'localized.groupGallery', - 'fandom' - ), - - groupData?.some((group) => group.directory === 'official') && - redirect( - 'Official - Gallery', - 'albums/official', - 'localized.groupGallery', - 'official' - ), - - wikiInfo.enableListings && - redirect( - 'Album Commentary', - 'list/all-commentary', - 'localized.commentaryIndex', - '' - ), - - wikiDataJSON && - writeFile( - path.join(outputPath, 'data.json'), - wikiDataJSON), - ].filter(Boolean)); - - async function redirect(title, from, urlKey, directory) { - const target = path.relative( - from, - urls.from('shared.root').to(urlKey, directory) - ); - const content = generateRedirectHTML(title, target, {language}); - await mkdir(path.join(outputPath, from), {recursive: true}); - await writeFile(path.join(outputPath, from, 'index.html'), content); - } -} -- cgit 1.3.0-6-gf8a5 From 154c050db72ebf06d4514326c696ab43fb3f8dc8 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 8 Jan 2023 11:49:44 -0400 Subject: basic actual stub for live-dev-server --- src/write/build-modes/live-dev-server.js | 43 ++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) (limited to 'src/write') diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 76b6d47f..c3094712 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -1,4 +1,9 @@ -import {logInfo} from '../../util/cli.js'; +import * as pageSpecs from '../../page/index.js'; + +import { + logInfo, + progressCallAll, +} from '../../util/cli.js'; export function getCLIOptions() { // Stub. @@ -16,16 +21,40 @@ export async function go({ _srcRootPath, _urls, _urlSpec, - _wikiData, + wikiData, _cachebust, _developersComment, _getSizeOfAdditionalFile, }) { - // Stub. - logInfo`So we are back in the mine!`; - logInfo`We are swinging our pickaxe in-`; - logInfo`...multiple directions,`; - logInfo`...multiple directions.`; + let targetSpecPairs = getPageSpecsWithTargets({wikiData}); + const writes = progressCallAll(`Computing page data & paths for ${targetSpecPairs.length} targets.`, + targetSpecPairs.map(({ + pageSpec, + target, + targetless, + }) => () => + targetless + ? pageSpec.writeTargetless({wikiData}) + : pageSpec.write(target, {wikiData}))).flat(); + + logInfo`Will be serving a total of ${writes.length} pages.`; + return true; } + +function getPageSpecsWithTargets({ + wikiData, +}) { + return Object.values(pageSpecs) + .filter(pageSpec => pageSpec.condition?.({wikiData}) ?? true) + .flatMap(pageSpec => [ + ...pageSpec.targets + ? pageSpec.targets({wikiData}) + .map(target => ({pageSpec, target})) + : [], + Object.hasOwn(pageSpec, 'writeTargetless') && + {pageSpec, targetless: true}, + ]) + .filter(Boolean); +} -- cgit 1.3.0-6-gf8a5 From 594e8dd46f9e6cc74c680536a1d820eef27133f0 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 9 Jan 2023 21:07:16 -0400 Subject: most essential behavior for live-dev-server --- src/write/bind-utilities.js | 1 + src/write/build-modes/live-dev-server.js | 287 +++++++++++++++++++++++++++++-- src/write/build-modes/static-build.js | 28 +-- src/write/page-template.js | 21 ++- 4 files changed, 303 insertions(+), 34 deletions(-) (limited to 'src/write') diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index 6632ba30..1c4dd282 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -61,6 +61,7 @@ export function bindUtilities({ const bound = {}; bound.html = html; + bound.language = language; bound.img = bindOpts(img, { [bindOpts.bindIndex]: 0, diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index c3094712..80badeb9 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -1,34 +1,55 @@ +import * as http from 'http'; +import {createReadStream} from 'fs'; +import {stat} from 'fs/promises'; +import * as path from 'path'; +import {pipeline} from 'stream/promises' + +import {bindUtilities} from '../bind-utilities.js'; + +import {serializeThings} from '../../data/serialize.js'; + import * as pageSpecs from '../../page/index.js'; +import {logInfo, progressCallAll} from '../../util/cli.js'; +import {withEntries} from '../../util/sugar.js'; + +import { + getPagePathname, + getPageSubdirectoryPrefix, + getURLsFrom, +} from '../../util/urls.js'; + import { - logInfo, - progressCallAll, -} from '../../util/cli.js'; + generateDocumentHTML, + generateGlobalWikiDataJSON, + generateRedirectHTML, +} from '../page-template.js'; export function getCLIOptions() { - // Stub. return {}; } export async function go({ _cliOptions, _dataPath, - _mediaPath, + mediaPath, _queueSize, - _defaultLanguage, - _languages, - _srcRootPath, - _urls, + defaultLanguage, + languages, + srcRootPath, + urls, _urlSpec, wikiData, - _cachebust, - _developersComment, - _getSizeOfAdditionalFile, + cachebust, + developersComment, + getSizeOfAdditionalFile, }) { + const port = 8002; + let targetSpecPairs = getPageSpecsWithTargets({wikiData}); - const writes = progressCallAll(`Computing page data & paths for ${targetSpecPairs.length} targets.`, + const pages = progressCallAll(`Computing page data & paths for ${targetSpecPairs.length} targets.`, targetSpecPairs.map(({ pageSpec, target, @@ -38,7 +59,245 @@ export async function go({ ? pageSpec.writeTargetless({wikiData}) : pageSpec.write(target, {wikiData}))).flat(); - logInfo`Will be serving a total of ${writes.length} pages.`; + logInfo`Will be serving a total of ${pages.length} pages.`; + + const urlToPageMap = Object.fromEntries(pages + .filter(page => page.type === 'page' || page.type === 'redirect') + .flatMap(page => { + let servePath; + if (page.type === 'page') + servePath = page.path; + else if (page.type === 'redirect') + servePath = page.fromPath; + + const fullKey = 'localized.' + servePath[0]; + const urlArgs = servePath.slice(1); + + return Object.values(languages).map(language => { + const baseDirectory = + language === defaultLanguage ? '' : language.code; + + const pathname = getPagePathname({ + baseDirectory, + fullKey, + urlArgs, + urls, + }); + + return [pathname, { + baseDirectory, + language, + page, + servePath, + }]; + }); + })); + + const server = http.createServer(async (request, response) => { + const contentTypeHTML = {'Content-Type': 'text/html; charset=utf-8'}; + const contentTypeJSON = {'Content-Type': 'text/json; charset=utf-8'}; + const contentTypePlain = {'Content-Type': 'text/plain; charset=utf-8'}; + + const requestTime = new Date().toLocaleDateString('en-US', {hour: '2-digit', minute: '2-digit', second: '2-digit'}); + const requestHead = `${requestTime} - ${request.socket.remoteAddress}`; + + let url; + try { + url = new URL(request.url, `http://${request.headers.host}`); + } catch (error) { + response.writeHead(500, contentTypePlain); + response.end('Failed to parse request URL\n'); + return; + } + + const {pathname} = url; + + // Specialized routes + + if (pathname === '/data.json') { + response.writeHead(200, contentTypeJSON); + response.end(generateGlobalWikiDataJSON({ + serializeThings, + wikiData, + })); + return; + } + + const { + area: localFileArea, + path: localFilePath + } = pathname.match(/^\/(?static|media)\/(?.*)/)?.groups ?? {}; + + if (localFileArea) { + // Not security tested, man, this is a dev server!! + const safePath = path.resolve('/', localFilePath).replace(/^\//, ''); + + let localDirectory; + if (localFileArea === 'static') { + localDirectory = path.join(srcRootPath, 'static'); + } else if (localFileArea === 'media') { + localDirectory = mediaPath; + } + + const filePath = path.resolve(localDirectory, safePath); + + try { + await stat(filePath); + } catch (error) { + if (error.code === 'ENOENT') { + response.writeHead(404, contentTypePlain); + response.end(`No ${localFileArea} file found for: ${safePath}`); + console.log(`${requestHead} [404] ${pathname}`); + console.log(`ENOENT for stat: ${filePath}`); + } else { + response.writeHead(500, contentTypePlain); + response.end(`Internal error accessing ${localFileArea} file for: ${safePath}`); + console.error(`${requestHead} [500] ${pathname}`); + console.error(error); + } + return; + } + + try { + response.writeHead(200); // Sorry, no MIME type for now + await pipeline( + createReadStream(filePath), + response); + console.log(`${requestHead} [200] ${pathname}`); + } catch (error) { + response.writeHead(500, contentTypePlain); + response.end(`Failed during file-to-response pipeline`); + console.error(`${requestHead} [500] ${pathname}`); + console.error(error); + return; + } + } + + // Other routes determined by page and URL specs + + // URL to page map expects trailing slash but no leading slash. + const pathnameKey = pathname.replace(/^\//, '') + (pathname.endsWith('/') ? '' : '/'); + + if (!Object.hasOwn(urlToPageMap, pathnameKey)) { + response.writeHead(404, contentTypePlain); + response.end(`No page found for: ${pathnameKey}\n`); + console.log(`${requestHead} [404] ${pathname}`); + return; + } + + const { + baseDirectory, + language, + page, + servePath, + } = urlToPageMap[pathnameKey]; + + const to = getURLsFrom({ + urls, + baseDirectory, + pageSubKey: servePath[0], + subdirectoryPrefix: getPageSubdirectoryPrefix({ + urlArgs: servePath.slice(1), + }), + }); + + const absoluteTo = (targetFullKey, ...args) => { + const [groupKey, subKey] = targetFullKey.split('.'); + const from = urls.from('shared.root'); + return ( + '/' + + (groupKey === 'localized' && baseDirectory + ? from.to( + 'localizedWithBaseDirectory.' + subKey, + baseDirectory, + ...args + ) + : from.to(targetFullKey, ...args)) + ); + }; + + try { + const pageSubKey = servePath[0]; + const urlArgs = servePath.slice(1); + + if (page.type === 'redirect') { + response.writeHead(301, contentTypeHTML); + + const target = to('localized.' + page.toPath[0], ...page.toPath.slice(1)); + const redirectHTML = generateRedirectHTML(page.title, target, {language}); + + response.end(redirectHTML); + + console.log(`${requestHead} [301] (redirect) ${pathname}`); + return; + } + + response.writeHead(200, contentTypeHTML); + + const localizedPathnames = withEntries(languages, entries => entries + .filter(([key, language]) => key !== 'default' && !language.hidden) + .map(([_key, language]) => [ + language.code, + getPagePathname({ + baseDirectory: + (language === defaultLanguage + ? '' + : language.code), + fullKey: 'localized.' + pageSubKey, + urlArgs, + urls, + }), + ])); + + const bound = bindUtilities({ + language, + to, + wikiData, + }); + + const pageInfo = page.page({ + ...bound, + + absoluteTo, + relativeTo: to, + to, + urls, + + getSizeOfAdditionalFile, + }); + + const pageHTML = generateDocumentHTML(pageInfo, { + cachebust, + defaultLanguage, + developersComment, + getThemeString: bound.getThemeString, + language, + languages, + localizedPathnames, + oEmbedJSONHref: null, // No oEmbed support for live dev server + pageSubKey, + pathname, + urlArgs, + to, + transformMultiline: bound.transformMultiline, + wikiData, + }); + + console.log(`${requestHead} [200] ${pathname}`); + response.end(pageHTML); + } catch (error) { + response.writeHead(500, contentTypePlain); + response.end(`Error generating page, view server log for details\n`); + console.error(`${requestHead} [500] ${pathname}`); + console.error(error); + } + }); + + server.listen(port); + logInfo`${'All done!'} Listening at ${`http://0.0.0.0:${port}/`}`; + + // Just keep going... forever!!! + await new Promise(() => {}); return true; } diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index b3700c43..1544a122 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -16,7 +16,6 @@ import * as pageSpecs from '../../page/index.js'; import link from '../../util/link.js'; import {empty, queue, withEntries} from '../../util/sugar.js'; -import {getPagePaths, getURLsFrom} from '../../util/urls.js'; import { logError, @@ -26,6 +25,13 @@ import { progressPromiseAll, } from '../../util/cli.js'; +import { + getPagePathname, + getPagePaths, + getPageSubdirectoryPrefix, + getURLsFrom, +} from '../../util/urls.js'; + const pageFlags = Object.keys(pageSpecs); export function getCLIOptions() { @@ -263,20 +269,18 @@ export async function go({ const pageSubKey = path[0]; const urlArgs = path.slice(1); - const localizedPaths = withEntries(languages, entries => entries + const localizedPathnames = withEntries(languages, entries => entries .filter(([key, language]) => key !== 'default' && !language.hidden) .map(([_key, language]) => [ language.code, - getPagePaths({ - outputPath, - urls, - + getPagePathname({ baseDirectory: (language === defaultLanguage ? '' : language.code), fullKey: 'localized.' + pageSubKey, urlArgs, + urls, }), ])); @@ -293,7 +297,9 @@ export async function go({ urls, baseDirectory, pageSubKey, - paths, + subdirectoryPrefix: getPageSubdirectoryPrefix({ + urlArgs: page.path.slice(1), + }), }); const absoluteTo = (targetFullKey, ...args) => { @@ -320,8 +326,6 @@ export async function go({ const pageInfo = page({ ...bound, - language, - absoluteTo, relativeTo: to, to, @@ -350,7 +354,7 @@ export async function go({ getThemeString: bound.getThemeString, language, languages, - localizedPaths, + localizedPathnames, oEmbedJSONHref, paths, to, @@ -382,7 +386,9 @@ export async function go({ urls, baseDirectory, pageSubKey: fromPath[0], - paths: from, + subdirectoryPrefix: getPageSubdirectoryPrefix({ + urlArgs: fromPath.slice(1), + }), }); const target = to('localized.' + toPath[0], ...toPath.slice(1)); diff --git a/src/write/page-template.js b/src/write/page-template.js index efbc5795..88d81c23 100644 --- a/src/write/page-template.js +++ b/src/write/page-template.js @@ -53,11 +53,13 @@ export function generateDocumentHTML(pageInfo, { getThemeString, language, languages, - localizedPaths, - paths, + localizedPathnames, oEmbedJSONHref, + pageSubKey, + pathname, to, transformMultiline, + urlArgs, wikiData, }) { const {wikiInfo} = wikiData; @@ -125,11 +127,11 @@ export function generateDocumentHTML(pageInfo, { : null; const canonical = wikiInfo.canonicalBase - ? wikiInfo.canonicalBase + (paths.pathname === '/' ? '' : paths.pathname) + ? wikiInfo.canonicalBase + (pathname === '/' ? '' : pathname) : ''; const localizedCanonical = wikiInfo.canonicalBase - ? Object.entries(localizedPaths).map(([code, {pathname}]) => ({ + ? Object.entries(localizedPathnames).map(([code, pathname]) => ({ lang: code, href: wikiInfo.canonicalBase + (pathname === '/' ? '' : pathname), })) @@ -162,13 +164,14 @@ export function generateDocumentHTML(pageInfo, { }, footer.content), - getFooterLocalizationLinks(paths.pathname, { + getFooterLocalizationLinks(pathname, { defaultLanguage, html, language, languages, - paths, + pageSubKey, to, + urlArgs, }), ]); @@ -264,7 +267,7 @@ export function generateDocumentHTML(pageInfo, { ? to(...cur.path) : cur.href ? (() => { - logWarn`Using legacy href format nav link in ${paths.pathname}`; + logWarn`Using legacy href format nav link in ${pathname}`; return cur.href; })() : null, @@ -447,9 +450,9 @@ export function generateDocumentHTML(pageInfo, { { lang: language.intlCode, 'data-language-code': language.code, - 'data-url-key': paths.urlPath[0], + 'data-url-key': 'localized.' + pageSubKey, ...Object.fromEntries( - paths.urlPath.slice(1).map((v, i) => [['data-url-value' + i], v]) + urlArgs.map((v, i) => [['data-url-value' + i], v]) ), 'data-rebase-localized': to('localized.root'), 'data-rebase-shared': to('shared.root'), -- cgit 1.3.0-6-gf8a5 From a532f066ffbe0387216c85323fdf81aff36c1d6c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 9 Jan 2023 21:07:37 -0400 Subject: finish up the rest necessary for live-dev-server --- src/write/build-modes/live-dev-server.js | 113 +++++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 19 deletions(-) (limited to 'src/write') diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index 80badeb9..f322a79b 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -10,7 +10,7 @@ import {serializeThings} from '../../data/serialize.js'; import * as pageSpecs from '../../page/index.js'; -import {logInfo, progressCallAll} from '../../util/cli.js'; +import {logInfo, logWarn, progressCallAll} from '../../util/cli.js'; import {withEntries} from '../../util/sugar.js'; import { @@ -26,27 +26,39 @@ import { } from '../page-template.js'; export function getCLIOptions() { - return {}; + return { + host: { + type: 'value', + }, + + port: { + type: 'value', + validate(size) { + if (parseInt(size) !== parseFloat(size)) return 'an integer'; + if (parseInt(size) < 1024 || parseInt(size) > 49151) return 'a user/registered port (1024-49151)'; + return true; + }, + }, + }; } export async function go({ - _cliOptions, + cliOptions, _dataPath, mediaPath, - _queueSize, defaultLanguage, languages, srcRootPath, urls, - _urlSpec, wikiData, cachebust, developersComment, getSizeOfAdditionalFile, }) { - const port = 8002; + const host = cliOptions['host'] ?? '0.0.0.0'; + const port = parseInt(cliOptions['port'] ?? 8002); let targetSpecPairs = getPageSpecsWithTargets({wikiData}); const pages = progressCallAll(`Computing page data & paths for ${targetSpecPairs.length} targets.`, @@ -95,7 +107,7 @@ export async function go({ const server = http.createServer(async (request, response) => { const contentTypeHTML = {'Content-Type': 'text/html; charset=utf-8'}; - const contentTypeJSON = {'Content-Type': 'text/json; charset=utf-8'}; + const contentTypeJSON = {'Content-Type': 'application/json; charset=utf-8'}; const contentTypePlain = {'Content-Type': 'text/plain; charset=utf-8'}; const requestTime = new Date().toLocaleDateString('en-US', {hour: '2-digit', minute: '2-digit', second: '2-digit'}); @@ -115,26 +127,35 @@ export async function go({ // Specialized routes if (pathname === '/data.json') { - response.writeHead(200, contentTypeJSON); - response.end(generateGlobalWikiDataJSON({ - serializeThings, - wikiData, - })); + try { + const json = generateGlobalWikiDataJSON({ + serializeThings, + wikiData, + }); + response.writeHead(200, contentTypeJSON); + response.end(json); + console.log(`${requestHead} [200] /data.json`); + } catch (error) { + response.writeHead(500, contentTypeJSON); + response.end({error: `Internal error serializing wiki JSON`}); + console.error(`${requestHead} [500] /data.json`); + console.error(error); + } return; } const { area: localFileArea, path: localFilePath - } = pathname.match(/^\/(?static|media)\/(?.*)/)?.groups ?? {}; + } = pathname.match(/^\/(?static|util|media)\/(?.*)/)?.groups ?? {}; if (localFileArea) { // Not security tested, man, this is a dev server!! const safePath = path.resolve('/', localFilePath).replace(/^\//, ''); let localDirectory; - if (localFileArea === 'static') { - localDirectory = path.join(srcRootPath, 'static'); + if (localFileArea === 'static' || localFileArea === 'util') { + localDirectory = path.join(srcRootPath, localFileArea); } else if (localFileArea === 'media') { localDirectory = mediaPath; } @@ -158,8 +179,42 @@ export async function go({ return; } + const extname = path.extname(safePath).slice(1).toLowerCase(); + + const contentType = { + // BRB covering all my bases + 'aac': 'audio/aac', + 'bmp': 'image/bmp', + 'css': 'text/css', + 'csv': 'text/csv', + 'gif': 'image/gif', + 'ico': 'image/vnd.microsoft.icon', + 'jpg': 'image/jpeg', + 'jpeg:': 'image/jpeg', + 'js': 'text/javascript', + 'mjs': 'text/javascript', + 'mp3': 'audio/mpeg', + 'mp4': 'video/mp4', + 'oga': 'audio/ogg', + 'ogg': 'audio/ogg', + 'ogv': 'video/ogg', + 'opus': 'audio/opus', + 'png': 'image/png', + 'pdf': 'application/pdf', + 'svg': 'image/svg+xml', + 'ttf': 'font/ttf', + 'txt': 'text/plain', + 'wav': 'audio/wav', + 'weba': 'audio/webm', + 'webm': 'video/webm', + 'woff': 'font/woff', + 'woff2': 'font/woff2', + 'xml': 'application/xml', + 'zip': 'application/zip', + }[extname]; + try { - response.writeHead(200); // Sorry, no MIME type for now + response.writeHead(200, contentType ? {'Content-Type': contentType} : {}); await pipeline( createReadStream(filePath), response); @@ -169,8 +224,8 @@ export async function go({ response.end(`Failed during file-to-response pipeline`); console.error(`${requestHead} [500] ${pathname}`); console.error(error); - return; } + return; } // Other routes determined by page and URL specs @@ -293,8 +348,28 @@ export async function go({ } }); - server.listen(port); - logInfo`${'All done!'} Listening at ${`http://0.0.0.0:${port}/`}`; + const address = `http://${host}:${port}/`; + + server.on('error', error => { + if (error.code === 'EADDRINUSE') { + logWarn`Port ${port} is already in use - will (continually) retry after 10 seconds.`; + logWarn`Press ^C here (control+C) to exit and change ${'--port'} number, or stop the server currently running on port ${port}.`; + setTimeout(() => { + server.close(); + server.listen(port, host); + }, 10_000); + } else { + console.error(`Server error detected (code: ${error.code})`); + console.error(error); + } + }); + + server.on('listening', () => { + logInfo`${'All done!'} Listening at: ${address}`; + logInfo`Press ^C here (control+C) to stop the server and exit.`; + }); + + server.listen(port, host); // Just keep going... forever!!! await new Promise(() => {}); -- cgit 1.3.0-6-gf8a5 From 0480e9a6502695fb741fc300db03ef734f38b51c Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Mon, 9 Jan 2023 21:42:25 -0400 Subject: attempt to fix path issues on windows --- src/write/build-modes/live-dev-server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/write') diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index f322a79b..a8e54247 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -151,7 +151,7 @@ export async function go({ if (localFileArea) { // Not security tested, man, this is a dev server!! - const safePath = path.resolve('/', localFilePath).replace(/^\//, ''); + const safePath = path.posix.resolve('/', localFilePath).replace(/^\//, ''); let localDirectory; if (localFileArea === 'static' || localFileArea === 'util') { @@ -160,7 +160,7 @@ export async function go({ localDirectory = mediaPath; } - const filePath = path.resolve(localDirectory, safePath); + const filePath = path.resolve(localDirectory, safePath.split('/').join(path.sep)); try { await stat(filePath); -- cgit 1.3.0-6-gf8a5 From d442546057f8280a141d4aa54f633a09c429e2d3 Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 10 Jan 2023 16:55:04 -0400 Subject: extract absoluteTo --- src/write/build-modes/live-dev-server.js | 20 +++++--------------- src/write/build-modes/static-build.js | 20 +++++--------------- 2 files changed, 10 insertions(+), 30 deletions(-) (limited to 'src/write') diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index a8e54247..d481b480 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -17,6 +17,7 @@ import { getPagePathname, getPageSubdirectoryPrefix, getURLsFrom, + getURLsFromRoot, } from '../../util/urls.js'; import { @@ -256,20 +257,10 @@ export async function go({ }), }); - const absoluteTo = (targetFullKey, ...args) => { - const [groupKey, subKey] = targetFullKey.split('.'); - const from = urls.from('shared.root'); - return ( - '/' + - (groupKey === 'localized' && baseDirectory - ? from.to( - 'localizedWithBaseDirectory.' + subKey, - baseDirectory, - ...args - ) - : from.to(targetFullKey, ...args)) - ); - }; + const absoluteTo = getURLsFromRoot({ + baseDirectory, + urls, + }); try { const pageSubKey = servePath[0]; @@ -314,7 +305,6 @@ export async function go({ ...bound, absoluteTo, - relativeTo: to, to, urls, diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index 1544a122..c2bb02f2 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -30,6 +30,7 @@ import { getPagePaths, getPageSubdirectoryPrefix, getURLsFrom, + getURLsFromRoot, } from '../../util/urls.js'; const pageFlags = Object.keys(pageSpecs); @@ -302,20 +303,10 @@ export async function go({ }), }); - const absoluteTo = (targetFullKey, ...args) => { - const [groupKey, subKey] = targetFullKey.split('.'); - const from = urls.from('shared.root'); - return ( - '/' + - (groupKey === 'localized' && baseDirectory - ? from.to( - 'localizedWithBaseDirectory.' + subKey, - baseDirectory, - ...args - ) - : from.to(targetFullKey, ...args)) - ); - }; + const absoluteTo = getURLsFromRoot({ + baseDirectory, + urls, + }); const bound = bindUtilities({ language, @@ -327,7 +318,6 @@ export async function go({ ...bound, absoluteTo, - relativeTo: to, to, urls, -- cgit 1.3.0-6-gf8a5 From 4de56d21db5004e5b2c2a49caedee0908b9c6a9f Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Tue, 10 Jan 2023 17:01:54 -0400 Subject: bind more utilities in bindUtilities --- src/write/bind-utilities.js | 14 ++++++++++++-- src/write/build-modes/live-dev-server.js | 13 ++++--------- src/write/build-modes/static-build.js | 13 ++++--------- 3 files changed, 20 insertions(+), 20 deletions(-) (limited to 'src/write') diff --git a/src/write/bind-utilities.js b/src/write/bind-utilities.js index 1c4dd282..4b037a91 100644 --- a/src/write/bind-utilities.js +++ b/src/write/bind-utilities.js @@ -51,8 +51,11 @@ import { } from '../util/wiki-data.js'; export function bindUtilities({ + absoluteTo, + getSizeOfAdditionalFile, language, to, + urls, wikiData, }) { // TODO: Is there some nicer way to define these, @@ -60,8 +63,15 @@ export function bindUtilities({ // each page? const bound = {}; - bound.html = html; - bound.language = language; + Object.assign(bound, { + absoluteTo, + getSizeOfAdditionalFile, + html, + language, + to, + urls, + wikiData, + }) bound.img = bindOpts(img, { [bindOpts.bindIndex]: 0, diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js index d481b480..b6bf662b 100644 --- a/src/write/build-modes/live-dev-server.js +++ b/src/write/build-modes/live-dev-server.js @@ -296,20 +296,15 @@ export async function go({ ])); const bound = bindUtilities({ + absoluteTo, + getSizeOfAdditionalFile, language, to, + urls, wikiData, }); - const pageInfo = page.page({ - ...bound, - - absoluteTo, - to, - urls, - - getSizeOfAdditionalFile, - }); + const pageInfo = page.page(bound); const pageHTML = generateDocumentHTML(pageInfo, { cachebust, diff --git a/src/write/build-modes/static-build.js b/src/write/build-modes/static-build.js index c2bb02f2..dab2598a 100644 --- a/src/write/build-modes/static-build.js +++ b/src/write/build-modes/static-build.js @@ -309,20 +309,15 @@ export async function go({ }); const bound = bindUtilities({ + absoluteTo, + getSizeOfAdditionalFile, language, to, + urls, wikiData, }); - const pageInfo = page({ - ...bound, - - absoluteTo, - to, - urls, - - getSizeOfAdditionalFile, - }); + const pageInfo = page(bound); const oEmbedJSON = generateOEmbedJSON(pageInfo, { language, -- cgit 1.3.0-6-gf8a5