From dc05143840770c2ca4bd92d4b1ceef68cbe2972d Mon Sep 17 00:00:00 2001 From: Joost De Cock Date: Wed, 19 Jul 2023 19:08:41 +0200 Subject: [PATCH 01/12] feat(sites): Overhauled prebuild system --- config/authors.mjs | 1 + config/dependencies.yaml | 2 +- config/scripts.yaml | 15 +- packages/new-design/package.json | 2 +- sites/dev/hooks/use-navigation.mjs | 110 +--------- sites/dev/package.json | 5 +- sites/dev/pages/[...slug].mjs | 4 +- sites/dev/prebuild.mjs | 93 ++++++++ sites/lab/package.json | 3 +- sites/org/package.json | 5 +- sites/org/prebuild.mjs | 90 ++++++++ sites/shared/hooks/use-navigation-helpers.mjs | 2 +- sites/shared/prebuild/contributors.mjs | 50 +++-- sites/shared/prebuild/crowdin.mjs | 22 +- sites/shared/prebuild/designs.mjs | 11 +- sites/shared/prebuild/docs.mjs | 3 +- sites/shared/prebuild/favicon.mjs | 20 +- sites/shared/prebuild/git.mjs | 78 ++++--- sites/shared/prebuild/i18n.mjs | 18 +- sites/shared/prebuild/index.mjs | 56 ----- sites/shared/prebuild/markdown.mjs | 200 ++++++++++++++++++ sites/shared/prebuild/navigation.mjs | 119 +++++++---- sites/shared/prebuild/og/index.mjs | 2 +- sites/shared/prebuild/patrons.mjs | 51 +++-- sites/shared/prebuild/runner.mjs | 136 ++++++++++++ sites/shared/prebuild/sitenav-dev.mjs | 54 +++++ sites/shared/utils.mjs | 11 + yarn.lock | 22 ++ 28 files changed, 874 insertions(+), 311 deletions(-) create mode 100644 sites/dev/prebuild.mjs create mode 100644 sites/org/prebuild.mjs delete mode 100644 sites/shared/prebuild/index.mjs create mode 100644 sites/shared/prebuild/markdown.mjs create mode 100644 sites/shared/prebuild/runner.mjs create mode 100644 sites/shared/prebuild/sitenav-dev.mjs diff --git a/config/authors.mjs b/config/authors.mjs index d2188510459..8b33f8af43b 100644 --- a/config/authors.mjs +++ b/config/authors.mjs @@ -5,6 +5,7 @@ */ export const authors = { joostdecock: { id: 0, name: 'Joost De Cock' }, + mocked: { id: 0, name: 'Unknown (mocked in dev)' }, benjamesben: { id: 0, name: 'Benjamin' }, nikhil: { id: 0, name: 'nikhil' }, jackseye: { id: 0, name: 'jackseye' }, diff --git a/config/dependencies.yaml b/config/dependencies.yaml index dc63426a9e4..500d11052d1 100644 --- a/config/dependencies.yaml +++ b/config/dependencies.yaml @@ -104,7 +104,7 @@ new-design: 'chalk': '5.0.1' 'execa': '7.1.1' 'mustache': '4.2.0' - 'ora': '6.1.0' + 'ora': '6.3.1' 'prompts': '2.4.2' 'recursive-readdir': '2.2.3' noble: diff --git a/config/scripts.yaml b/config/scripts.yaml index 1210e5a9e6f..eb44ffa003f 100644 --- a/config/scripts.yaml +++ b/config/scripts.yaml @@ -24,8 +24,6 @@ core: prettier: "npx prettier --write 'src/*.mjs' 'tests/*.mjs'" lint: "npx eslint 'src/*.mjs' 'tests/*.mjs'" jsdoc: 'jsdoc -c jsdoc.json -r src' -i18n: - prebuild: 'node scripts/prebuilder.mjs' models: test: 'npx mocha tests/*.test.mjs' new-design: @@ -67,13 +65,14 @@ dev: build: &nextBuild 'next build' cibuild: 'yarn build && node scripts/algolia.mjs' clean: &nextClean 'rimraf prebuild/* && rimraf public/locales/*/* && rimraf public/feeds/* && rimraf ../shared/prebuild/data/*' - predev: 'FAST=1 SITE=dev node --experimental-json-modules ../shared/prebuild/index.mjs' dev: &nextDev 'next dev -p 8000' develop: *nextDev i18n: "SITE=dev node ../shared/prebuild/i18n-only.mjs" lint: &nextLint 'next lint' buildsitedeps: &buildsitedeps 'cd ../../ && yarn buildall && cd -' - prebuild: 'yarn buildsitedeps && SITE=dev node --experimental-json-modules ../shared/prebuild/index.mjs' + prebuild: 'yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs' + prebuildonly: 'node --experimental-json-modules ./prebuild.mjs' + predev: 'node --experimental-json-modules ./prebuild.mjs' serve: "pm2 start npm --name 'dev' -- run start" start: &nextStart 'yarn prebuild && yarn dev' @@ -91,20 +90,22 @@ lab: e2e: &e2e 'yarn playwright test' lint: *nextLint buildsitedeps: *buildsitedeps - prebuild: 'yarn buildsitedeps && SITE=lab node --experimental-json-modules ../shared/prebuild/index.mjs' + prebuild: 'yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs' + prebuildonly: 'node --experimental-json-modules ./prebuild.mjs' start: *nextStart org: build: *nextBuild cibuild: 'yarn build' clean: *nextClean - predev: 'FAST=1 SITE=org node --experimental-json-modules ../shared/prebuild/index.mjs' dev: *nextDev develop: *nextDev i18n: 'SITE=org node ../shared/prebuild/i18n-only.mjs' lint: *nextLint buildsitedeps: *buildsitedeps - prebuild: 'yarn buildsitedeps && SITE=org node --experimental-json-modules ../shared/prebuild/index.mjs' + prebuild: 'yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs' + prebuildonly: 'node --experimental-json-modules ./prebuild.mjs' + predev: 'node --experimental-json-modules ./prebuild.mjs' start: *nextStart sanity: diff --git a/packages/new-design/package.json b/packages/new-design/package.json index 8c56ff7ab24..8655eaa731a 100644 --- a/packages/new-design/package.json +++ b/packages/new-design/package.json @@ -39,7 +39,7 @@ "chalk": "5.0.1", "execa": "7.1.1", "mustache": "4.2.0", - "ora": "6.1.0", + "ora": "6.3.1", "prompts": "2.4.2", "recursive-readdir": "2.2.3" }, diff --git a/sites/dev/hooks/use-navigation.mjs b/sites/dev/hooks/use-navigation.mjs index e4f7f2ace42..4d1fae9f676 100644 --- a/sites/dev/hooks/use-navigation.mjs +++ b/sites/dev/hooks/use-navigation.mjs @@ -1,108 +1,4 @@ -import { prebuildNavigation as pbn } from 'site/prebuild/navigation.mjs' -import { orderedSlugLut } from 'shared/hooks/use-navigation-helpers.mjs' +import { siteNav } from 'site/prebuild/navigation.en.mjs' +import { slugLut } from 'site/prebuild/sluglut.en.mjs' -/* - * prebuildNavvigation[locale] holds the navigation structure based on MDX content. - * The entire website only has a few pages that are now MDX-based: - * - 404 => no navigation shown - * - home page => no navvigation shown - * - /contact => Added below - * - * Remember Mc_Shifton: - * Note: Set 'm' to truthy to show this as a main section in the side-navigation (optional) - * Note: Set 'c' to set the control level to hide things from users (optional) - * Note: Set 's' to the slug (optional insofar as it's not a real page (a spacer for the header)) - * Note: Set '_' to never show the page in the site navigation (like the tags pages) - * Note: Set 'h' to indicate this is a top-level page that should be hidden from the side-nav (like search) - * Note: Set 'i' when something should be included as top-level in the collapse side-navigation (optional) - * Note: Set 'f' to add the page to the footer - * Note: Set 't' to the title - * Note: Set 'o' to set the order (optional) - * Note: Set 'n' to mark this as a noisy entry that should always be closed unless active (like blog) - */ - -export const ns = ['account', 'sections', 'design', 'tags'] - -const sitePages = () => { - const pages = { - // Top-level pages that are the sections menu - api: { - m: 1, - s: 'api', - t: 'API Documentation', - o: 10, - }, - design: { - m: 1, - s: 'design', - t: 'Design Sewing Patterns', - o: 10, - }, - contribute: { - m: 1, - s: 'contribute', - t: 'Contribute to FreeSewing', - o: 20, - }, - i18n: { - m: 1, - s: 'i18n', - t: 'Help Translate FreeSewing', - o: 40, - }, - infra: { - m: 1, - s: 'infra', - t: 'FreeSewing Infrastructure', - o: 50, - }, - about: { - m: 1, - s: 'about', - f: 1, - t: 'About FreeSewing', - o: 60, - }, - support: { - m: 1, - s: 'support', - f: 1, - t: 'Support FreeSewing', - o: 70, - }, - search: { - s: 'search', - h: 1, - f: 1, - t: 'Search', - o: 270, - }, - sitemap: { - s: 'sitemap', - h: 1, - f: 1, - t: 'Sitemap', - o: 270, - }, - } - return pages -} - -export const useNavigation = () => { - // Dev is EN only - const siteNav = { ...pbn.en, ...sitePages() } - - // Make top-level documentation entries appear in i-list - for (const page of ['tutorials', 'guides', 'howtos', 'reference', 'training']) { - siteNav[page].o = 1000 - siteNav[page].i = 1 - } - - // Hide contact from the sitenav - siteNav.contact.h = 1 - - return { - siteNav, // Site navigation - slugLut: orderedSlugLut(siteNav), // Slug lookup table - } -} +export const useNavigation = () => ({ siteNav, slugLut }) diff --git a/sites/dev/package.json b/sites/dev/package.json index b46930ac483..4eff5a39359 100644 --- a/sites/dev/package.json +++ b/sites/dev/package.json @@ -17,13 +17,14 @@ "build": "next build", "cibuild": "yarn build && node scripts/algolia.mjs", "clean": "rimraf prebuild/* && rimraf public/locales/*/* && rimraf public/feeds/* && rimraf ../shared/prebuild/data/*", - "predev": "FAST=1 SITE=dev node --experimental-json-modules ../shared/prebuild/index.mjs", "dev": "next dev -p 8000", "develop": "next dev -p 8000", "i18n": "SITE=dev node ../shared/prebuild/i18n-only.mjs", "lint": "next lint", "buildsitedeps": "cd ../../ && yarn buildall && cd -", - "prebuild": "yarn buildsitedeps && SITE=dev node --experimental-json-modules ../shared/prebuild/index.mjs", + "prebuild": "yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs", + "prebuildonly": "node --experimental-json-modules ./prebuild.mjs", + "predev": "node --experimental-json-modules ./prebuild.mjs", "serve": "pm2 start npm --name 'dev' -- run start", "start": "yarn prebuild && yarn dev" }, diff --git a/sites/dev/pages/[...slug].mjs b/sites/dev/pages/[...slug].mjs index 7e8a8daeb04..357ec4e1915 100644 --- a/sites/dev/pages/[...slug].mjs +++ b/sites/dev/pages/[...slug].mjs @@ -1,5 +1,5 @@ // Used in static paths -import { mdxPaths } from 'site/prebuild/mdx-paths.en.mjs' +import { pages } from 'site/prebuild/docs.en.mjs' // Dependencies import { serverSideTranslations } from 'next-i18next/serverSideTranslations' // Hooks @@ -126,7 +126,7 @@ export async function getStaticProps({ params }) { */ export async function getStaticPaths() { return { - paths: mdxPaths.map((slug) => '/' + slug), + paths: Object.keys(pages).map((slug) => '/' + slug), fallback: false, } } diff --git a/sites/dev/prebuild.mjs b/sites/dev/prebuild.mjs new file mode 100644 index 00000000000..9d672783a68 --- /dev/null +++ b/sites/dev/prebuild.mjs @@ -0,0 +1,93 @@ +import { prebuildRunner } from '../shared/prebuild/runner.mjs' + +/* + * This handles the prebuild step for FreeSewing.dev + * It runs via an NPM run script, so in a pure NodeJS context + */ +prebuildRunner({ + /* + * Pass the site to the runner + */ + site: 'dev', + + /* + * This prebuild config determines which prebuild step to run + * For each step, the options are: + * + * - true: Always run + * - false: Never run + * - 'productionOnly': Run in production, mock or skip in develop to save time + * + */ + prebuild: { + // ALWAYS PREBUILD //////////////////////////////////////////////////////// + + /* + * Always prebuild the designs + */ + designs: true, + + /* + * Always prebuild the MDX documentation + */ + docs: true, + + /* + * Always prebuild the translation files + * Even if we only support English on FreeSewing.dev, + * we still rely on the (English) translation of strings + */ + i18n: true, + + /* + * Always prebuild the navigation object (sitenav) and slug lookup tables (sluglut) + */ + navigation: true, + + // PREBUILD IN PRUDUCTION - MOCK/SKIP IN DEV /////////////////////////////// + + /* + * Only prebuild the contributor info in production + * Will be mocked in development mode to save time + */ + contributors: 'productionOnly', + + /* + * Only prebuild the crowdin info (translation statistics) in production + * Will be mocked in development mode to save time + */ + crowdin: 'productionOnly', + + /* + * Only prebuild the favicon files in production + * Will be mocked in development mode to save time + */ + favicon: 'productionOnly', + + /* + * Only prebuild the git author info in production + * Will be mocked in development mode to save time + */ + git: 'productionOnly', + + /* + * Only prebuild the Open Graph (og) images in production + * Will be skipped in development mode to save time + */ + ogImages: 'productionOnly', + + /* + * Only prebuild the patron info in production + * Will be mocked in development mode to save time + */ + patrons: 'productionOnly', + + // NEVER PREBUILD ////////////////////////////////////////////////////////// + + /* + * Never prebuild the MDX posts because there are not on FreeSewing.dev + * We could have leave this out, but it's included here for documenation purposes + */ + posts: false, + }, +}) diff --git a/sites/lab/package.json b/sites/lab/package.json index a84150575b1..fd18cd8725d 100644 --- a/sites/lab/package.json +++ b/sites/lab/package.json @@ -23,7 +23,8 @@ "e2e": "yarn playwright test", "lint": "next lint", "buildsitedeps": "cd ../../ && yarn buildall && cd -", - "prebuild": "yarn buildsitedeps && SITE=lab node --experimental-json-modules ../shared/prebuild/index.mjs", + "prebuild": "yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs", + "prebuildonly": "node --experimental-json-modules ./prebuild.mjs", "start": "yarn prebuild && yarn dev" }, "peerDependencies": {}, diff --git a/sites/org/package.json b/sites/org/package.json index 20749afc84c..2bd9de501ff 100644 --- a/sites/org/package.json +++ b/sites/org/package.json @@ -17,13 +17,14 @@ "build": "next build", "cibuild": "yarn build", "clean": "rimraf prebuild/* && rimraf public/locales/*/* && rimraf public/feeds/* && rimraf ../shared/prebuild/data/*", - "predev": "FAST=1 SITE=org node --experimental-json-modules ../shared/prebuild/index.mjs", "dev": "next dev -p 8000", "develop": "next dev -p 8000", "i18n": "SITE=org node ../shared/prebuild/i18n-only.mjs", "lint": "next lint", "buildsitedeps": "cd ../../ && yarn buildall && cd -", - "prebuild": "yarn buildsitedeps && SITE=org node --experimental-json-modules ../shared/prebuild/index.mjs", + "prebuild": "yarn buildsitedeps && node --experimental-json-modules ./prebuild.mjs", + "prebuildonly": "node --experimental-json-modules ./prebuild.mjs", + "predev": "node --experimental-json-modules ./prebuild.mjs", "start": "yarn prebuild && yarn dev" }, "peerDependencies": {}, diff --git a/sites/org/prebuild.mjs b/sites/org/prebuild.mjs new file mode 100644 index 00000000000..888bc9c9fd8 --- /dev/null +++ b/sites/org/prebuild.mjs @@ -0,0 +1,90 @@ +import { prebuildRunner } from '../shared/prebuild/runner.mjs' + +/* + * This handles the prebuild step for FreeSewing.org + * It runs via an NPM run script, so in a pure NodeJS context + */ +prebuildRunner({ + /* + * Pass the site to the runner + */ + site: 'org', + + /* + * This prebuild config determines which prebuild step to run + * For each step, the options are: + * + * - true: Always run + * - false: Never run + * - 'productionOnly': Run in production, mock or skip in develop to save time + * + */ + prebuild: { + // ALWAYS PREBUILD //////////////////////////////////////////////////////// + + /* + * Always prebuild the designs + */ + designs: true, + + /* + * Always prebuild the MDX documentation + */ + docs: true, + + /* + * Always prebuild the MDX posts + */ + posts: true, + + /* + * Always prebuild the translation files + * Even if we only support English on FreeSewing.dev, + * we still rely on the (English) translation of strings + */ + i18n: true, + + /* + * Always prebuild the navigation object (sitenav) and slug lookup tables (sluglut) + */ + navigation: true, + + // PREBUILD IN PRUDUCTION - MOCK/SKIP IN DEV /////////////////////////////// + + /* + * Only prebuild the contributor info in production + * Will be mocked in development mode to save time + */ + contributors: 'productionOnly', + + /* + * Only prebuild the crowdin info (translation statistics) in production + * Will be mocked in development mode to save time + */ + crowdin: 'productionOnly', + + /* + * Only prebuild the favicon files in production + * Will be mocked in development mode to save time + */ + favicon: 'productionOnly', + + /* + * Only prebuild the git author info in production + * Will be mocked in development mode to save time + */ + git: 'productionOnly', + + /* + * Only prebuild the Open Graph (og) images in production + * Will be skipped in development mode to save time + */ + ogImages: 'productionOnly', + + /* + * Only prebuild the patron info in production + * Will be mocked in development mode to save time + */ + patrons: 'productionOnly', + }, +}) diff --git a/sites/shared/hooks/use-navigation-helpers.mjs b/sites/shared/hooks/use-navigation-helpers.mjs index 143515f57d2..645ab81fd3d 100644 --- a/sites/shared/hooks/use-navigation-helpers.mjs +++ b/sites/shared/hooks/use-navigation-helpers.mjs @@ -1,4 +1,4 @@ -import { pageHasChildren } from 'shared/utils.mjs' +import { pageHasChildren } from '../utils.mjs' import orderBy from 'lodash.orderby' /* diff --git a/sites/shared/prebuild/contributors.mjs b/sites/shared/prebuild/contributors.mjs index fb73ffc83a3..6d49d0726c0 100644 --- a/sites/shared/prebuild/contributors.mjs +++ b/sites/shared/prebuild/contributors.mjs @@ -1,25 +1,45 @@ import path from 'path' -import fs from 'fs' +import { readFile, writeFile } from 'node:fs/promises' /* - * Main method that does what needs doing + * Prebuilds the list of all contributors */ -export const prebuildContributors = async(site) => { +export const prebuildContributors = async (store, mock = false) => { + if (mock) return (store.contributors = mockedData) - // Say hi - console.log() - console.log(`Prebuilding contributor list for freesewing.${site}`) + /* + * Read from all-contributors configuration file + */ + const contributorsFile = await readFile(path.resolve('..', '..', '.all-contributorsrc'), 'utf-8') - // Read from rc file - const contributors = JSON.parse(fs.readFileSync( - path.resolve('..', '..', '.all-contributorsrc'), - 'utf-8' - )) + /* + * Parse as JSON and get contributors list + */ + const { contributors } = JSON.parse(contributorsFile) - // Write to json - fs.writeFileSync( - path.resolve('..', site, 'prebuild', `allcontributors.js`), - `export default ${JSON.stringify(contributors.contributors, null ,2)}` + /* + * Update the store + */ + store.contributors = contributors + + /* + * Write out prebuild results + */ + return await writeFile( + path.resolve('..', store.site, 'prebuild', `allcontributors.js`), + `export default ${JSON.stringify(contributors, null, 2)}` ) } +/* + * In development, we return this mocked data to speed things up + */ +const mockedData = [ + { + login: 'joostdecock', + name: 'Joost De Cock', + avatar_url: 'https://avatars.githubusercontent.com/u/1708494?v=4', + profile: 'https://joost.at/', + contributions: ['maintenance'], + }, +] diff --git a/sites/shared/prebuild/crowdin.mjs b/sites/shared/prebuild/crowdin.mjs index 5aa083b957c..7daa031c0b4 100644 --- a/sites/shared/prebuild/crowdin.mjs +++ b/sites/shared/prebuild/crowdin.mjs @@ -43,18 +43,17 @@ const sendApiRequest = async (url = '', body = false, download = false) => { return false } -//const loadProjectMembers = async () => await sendApiRequest('members?limit=100') - const loadTopMembers = async (languageId) => await sendApiRequest('reports?limit=500', { ...report, schema: { ...report.schema, languageId } }) const checkReportStatus = async (id) => await sendApiRequest(`reports/${id}`) const getReportUrl = async (id) => await sendApiRequest(`reports/${id}/download`) const downloadReport = async (url) => await sendApiRequest('', false, url) -export const prebuildCrowdin = async () => { +export const prebuildCrowdin = async (store, mock = false) => { + if (mock) return (store.crowdin = mockedData) + const contributions = {} for (let language of languages) { - console.log(`Loading translator contributions for ${language}`) contributions[language] = {} const report = await loadTopMembers(language) const id = report.identifier @@ -85,6 +84,19 @@ export const prebuildCrowdin = async () => { path.resolve('..', 'org', 'prebuild', 'translators.json'), JSON.stringify(contributions) ) + + store.crowdin = contributions + + return } -//prebuildCrowdin() +/* + * In development, we return this mocked data to speed things up + */ +const mockedData = { + nl: { 'Joost De Cock (joostdecock)': { translated: 16427 } }, + fr: { bret76: { translated: 36800 } }, + de: { starf: { translated: 22370 } }, + uk: { 'Morgan Frost (KaerMorhan)': { translated: 10505 } }, + es: { 'Sara Latorre (Tyrannogina)': { translated: 6713 } }, +} diff --git a/sites/shared/prebuild/designs.mjs b/sites/shared/prebuild/designs.mjs index f0c72a031ba..5ec09bced0b 100644 --- a/sites/shared/prebuild/designs.mjs +++ b/sites/shared/prebuild/designs.mjs @@ -17,7 +17,7 @@ async function loadDesign(design) { return result } -export const prebuildDesigns = async () => { +export const prebuildDesigns = async (store) => { const promises = [] const designs = [] @@ -36,6 +36,13 @@ export const prebuildDesigns = async () => { options[design] = config.options } + // Update the store + store.designs = { + designs, + options, + measurements, + } + // Write out prebuild files const header = '// This file is auto-generated by the prebuild script | Any changes will be overwritten\n' @@ -57,5 +64,5 @@ export const prebuildDesigns = async () => { ) ) - await Promise.all(promises) + return await Promise.all(promises) } diff --git a/sites/shared/prebuild/docs.mjs b/sites/shared/prebuild/docs.mjs index e31af85ad77..875849aa7b5 100644 --- a/sites/shared/prebuild/docs.mjs +++ b/sites/shared/prebuild/docs.mjs @@ -1,3 +1,4 @@ +import allLocales from '../../../config/languages.json' assert { type: 'json' } import fs from 'fs' import path from 'path' import rdir from 'recursive-readdir' @@ -103,7 +104,7 @@ export const prebuildDocs = async (site) => { const mdxRoot = path.resolve(...root) // Languages - const locales = site === 'dev' ? ['en'] : ['en', 'fr', 'es', 'nl', 'de'] + const locales = site === 'dev' ? ['en'] : allLocales const pages = {} // Loop over languages diff --git a/sites/shared/prebuild/favicon.mjs b/sites/shared/prebuild/favicon.mjs index be9b3724415..cbcc9dd8612 100644 --- a/sites/shared/prebuild/favicon.mjs +++ b/sites/shared/prebuild/favicon.mjs @@ -1,4 +1,4 @@ -import { copyFile } from 'node:fs/promises'; +import { copyFile } from 'node:fs/promises' import path from 'path' const files = [ @@ -14,22 +14,16 @@ const files = [ 'site.webmanifest', ] -/* - * Main method that does what needs doing - */ -export const prebuildFavicon = async (site) => { - // Say hi - console.log() - console.log(`Copying favicon data for FreeSewing.${site}`) - +export const prebuildFavicon = async (store) => { // Setup from/to folders const from = ['..', 'shared', 'favicon'] - const to = ['..', site, 'public'] + const to = ['..', store.site, 'public'] const promises = [] - for (const file of files) promises.push( - copyFile(path.resolve(...from, file), path.resolve(...to, file)) - ) + for (const file of files) + promises.push(copyFile(path.resolve(...from, file), path.resolve(...to, file))) + + store.favicon = files return Promise.all(promises) } diff --git a/sites/shared/prebuild/git.mjs b/sites/shared/prebuild/git.mjs index 6799efd4802..52972c300e0 100644 --- a/sites/shared/prebuild/git.mjs +++ b/sites/shared/prebuild/git.mjs @@ -3,6 +3,7 @@ import { gitToAuthor, authors as authorInfo } from '../../../config/authors.mjs' import path from 'path' import fs from 'fs' import { getMdxFileList, fileToSlug } from './docs.mjs' +import { yyyymmdd } from '../utils.mjs' const divider = '____' @@ -52,50 +53,73 @@ export const getGitMetadata = async (file, site) => { } } +/* + * Writes data to the prebuild files + */ +const writeData = async (store) => { + // Write page to disk + const dir = path.resolve('..', store.site, 'prebuild') + fs.mkdirSync(dir, { recursive: true }) + fs.writeFileSync( + path.resolve(dir, `doc-updates.mjs`), + `export const docUpdates = ${JSON.stringify(store.git.pages)}` + ) + + fs.writeFileSync( + path.resolve(dir, `doc-stats.mjs`), + `export const docStats = ${JSON.stringify(store.git.stats)}` + ) +} + /* * Main method that does what needs doing */ -export const prebuildGitData = async (site) => { - // Say hi - console.log() - console.log(`Prebuilding git author data for freesewing.${site}`) +export const prebuildGitData = async (store, mock) => { + if (mock) { + store.git = mockedData(store) + return writeData(store) + } // Setup MDX root path - const root = ['..', '..', 'markdown', site] - if (site === 'org') root.push('docs') + const root = ['..', '..', 'markdown', store.site] + if (store.site === 'org') root.push('docs') const mdxRoot = path.resolve(...root) - const pages = {} + store.git = { + pages: {}, + stats: {}, + } // Get list of filenames const list = await getMdxFileList(mdxRoot, 'en') // Loop over files for (const file of list) { - const { lastUpdated, authors, slug } = await getGitMetadata(file, site) - pages[slug] = { u: lastUpdated, a: [...authors] } + const { lastUpdated, authors, slug } = await getGitMetadata(file, store.site) + store.git.pages[slug] = { u: lastUpdated, a: [...authors] } } - // Write page to disk - const dir = path.resolve('..', site, 'prebuild') - fs.mkdirSync(dir, { recursive: true }) - fs.writeFileSync( - path.resolve(dir, `doc-updates.mjs`), - `export const docUpdates = ${JSON.stringify(pages)}` - ) // How about some stats - const stats = {} - for (const slug in pages) { - for (const author of pages[slug].a) { - if (typeof stats[author] === 'undefined') stats[author] = 0 - stats[author]++ + for (const slug in store.git.pages) { + for (const author of store.git.pages[slug].a) { + if (typeof store.git.stats[author] === 'undefined') store.git.stats[author] = 0 + store.git.stats[author]++ } } - fs.writeFileSync( - path.resolve(dir, `doc-stats.mjs`), - `export const docStats = ${JSON.stringify(stats, null, 2)}` - ) - - return pages + return writeData(store) +} + +/* + * In development, we return this mocked data to speed things up + */ +const mockedData = (store) => { + const pages = {} + const u = yyyymmdd() + for (const slug of store.navigation.sluglut.en) pages[slug] = { u, a: ['mocked'] } + + return { + pages, + stats: { mocked: store.navigation.sluglut.en.length }, + } } diff --git a/sites/shared/prebuild/i18n.mjs b/sites/shared/prebuild/i18n.mjs index 3107b9c247f..6ce6d276279 100644 --- a/sites/shared/prebuild/i18n.mjs +++ b/sites/shared/prebuild/i18n.mjs @@ -168,24 +168,25 @@ const patternTranslationAsNamespace = (i18n, language) => { /* * The method that does the actual work */ -export const prebuildI18n = async (site) => { +export const prebuildI18n = async (store) => { /* * FreeSewing.dev is only available in English */ - const languages = site === 'dev' ? ['en'] : allLanguages + const languages = store.site === 'dev' ? ['en'] : allLanguages /* * Handle code-adjacent translations (for React components and so on) */ - const files = await getI18nFileList(site, languages) + const files = await getI18nFileList(store.site, languages) const data = filesAsNamespaces(files) const namespaces = fixData(data, languages) // Write out code-adjacent source files for (const language of languages) { // Fan out into namespaces for (const namespace in namespaces) - writeJson(site, language, namespace, namespaces[namespace][language]) + writeJson(store.site, language, namespace, namespaces[namespace][language]) } + /* * Handle design translations */ @@ -197,8 +198,13 @@ export const prebuildI18n = async (site) => { const content = patternTranslationAsNamespace(designs[design], language) designNs[language][`${design}.t`] = content.t designNs[language][`${design}.d`] = content.d - writeJson(site, language, design, content) + writeJson(store.site, language, design, content) } } - for (const language of languages) writeJson(site, language, 'designs', designNs[language]) + for (const language of languages) writeJson(store.site, language, 'designs', designNs[language]) + + /* + * Update the store + */ + store.i18n = { namespaces, designNs } } diff --git a/sites/shared/prebuild/index.mjs b/sites/shared/prebuild/index.mjs deleted file mode 100644 index 6853b52d290..00000000000 --- a/sites/shared/prebuild/index.mjs +++ /dev/null @@ -1,56 +0,0 @@ -import { prebuildDocs } from './docs.mjs' -import { prebuildNavigation } from './navigation.mjs' -import { prebuildGitData } from './git.mjs' -import { prebuildContributors } from './contributors.mjs' -import { prebuildPatrons } from './patrons.mjs' -import { prebuildI18n } from './i18n.mjs' -import { prebuildLab } from './lab.mjs' -import { prebuildDesigns } from './designs.mjs' -import { prebuildFavicon } from './favicon.mjs' -import { generateOgImage } from './og/index.mjs' -import { loadSanityPosts } from './sanity.mjs' -import { prebuildCrowdin } from './crowdin.mjs' - -const run = async () => { - if (process.env.LINTER) return true - const FAST = process.env.FAST ? true : false - const SITE = process.env.SITE || 'lab' - await prebuildDesigns() - if (['org', 'dev'].includes(SITE)) { - if (!FAST) await prebuildGitData(SITE) - const docPages = await prebuildDocs(SITE) - await prebuildCrowdin() - const sanityPosts = await loadSanityPosts(SITE) - prebuildNavigation(docPages, sanityPosts, SITE) - if (!FAST && process.env.GENERATE_OG_IMAGES) { - // Create og image for the home page - await generateOgImage({ - lang: 'en', - site: SITE, - slug: '', - title: 'FreeSewing.dev', - }) - // Create og image for the 404 page - await generateOgImage({ - lang: 'en', - site: SITE, - slug: '/404', - intro: "There's nothing here. Only this message to say there's nothing here.", - title: 'Page not found', - lead: '404', - }) - } - } else { - await prebuildLab() - } - - await prebuildI18n(SITE) - if (!FAST) { - await prebuildContributors(SITE) - await prebuildPatrons(SITE) - await prebuildFavicon(SITE) - } - console.log() -} - -run() diff --git a/sites/shared/prebuild/markdown.mjs b/sites/shared/prebuild/markdown.mjs new file mode 100644 index 00000000000..e1e733a27b3 --- /dev/null +++ b/sites/shared/prebuild/markdown.mjs @@ -0,0 +1,200 @@ +import fs from 'node:fs' +import path from 'node:path' +import { exec } from 'node:child_process' + +/* + * Shared header to include in written .mjs files + */ +export const header = `/* + * This file was auto-generated by the prebuild script + * Any changes you make to it will be lost on the next (pre)build. + */ +` + +/* + * Strips quptes from the start/end of a string + */ +const stripQuotes = (str) => { + str = str.trim() + if (str.slice(0, 1) === '"') str = str.slice(1) + if (str.slice(-1) === '"') str = str.slice(0, -1) + + return str.trim() +} + +/* + * This is the fast and low-tech way to some frontmatter from all files in a folder + */ +const loadFolderFrontmatter = async (key, site, folder, transform = false) => { + const prefix = site === 'org' ? 'docs/' : '' + /* + * Figure out what directory to spawn the child process in + */ + const cwd = await path.resolve(process.cwd(), '..', '..', 'markdown', site, folder) + let list = false + const grep = exec('grep ^' + key + ': -RIsm 1 ' + cwd, { cwd }, (error, stdout, stderr) => { + if (error) { + console.error(`exec error: ${error}`) + return + } + + return stdout + }) + + /* + * Stdout is buffered, so we need to gather all of it + */ + let stdout = '' + for await (const data of grep.stdout) stdout += data + + /* + * Turn all matches into an array + */ + const matches = stdout.split('\n') + + /* + * Turns matches into structured data + */ + const pages = {} + for (let match of matches) { + /* + * Trim some of the irrelevant path info prior to splitting on '.md:{key}:' + */ + const chunks = match + .split(`markdown/${site}/${site === 'org' ? 'docs/' : ''}`) + .pop() + .split(`.md:${key}:`) + if (chunks.length === 2 && chunks[0].length > 1) { + /* + * Figure out the language and make sure we have an key for that language + */ + const lang = chunks[0].slice(-2) + if (!pages[lang]) pages[lang] = {} + + /* + * Add page to our object with slug as key and title as value + */ + + let slug = prefix + chunks[0].slice(0, -3) + if (slug === prefix) slug = slug.slice(0, -1) + pages[lang][slug] = transform + ? transform(stripQuotes(chunks[1]), slug, lang) + : stripQuotes(chunks[1]) + } + } + + return pages +} + +/* + * Merges in order key on those slugs that have it set + */ +const mergeOrder = (titles, order) => { + const pages = {} + for (const lang in titles) { + pages[lang] = {} + for (const [slug, t] of Object.entries(titles[lang])) { + pages[lang][slug] = { t } + if (order[slug]) pages[lang][slug] = order[slug] + } + } + + return pages +} + +/* + * Fixes the date format to be yyyymmdd + */ +const formatDate = (date, slug, lang) => { + date = date.split('-') + if (date.length === 1) date = date[0].split('.') + if (date.length === 1) console.log(`Could not format date ${date} from ${slug} (${lang})`) + else { + if (date[0].length === 4) return date.join('') + else return date.reverse().join('') + } +} + +/* + * Loads all docs files, titles and order + */ +const loadDocs = async (site) => { + const folder = site === 'org' ? 'docs' : '.' + const titles = await loadFolderFrontmatter('title', site, folder) + const order = await loadFolderFrontmatter('order', site, folder) + + return mergeOrder(titles, order) +} + +/* + * Loads all blog posts, titles and order + */ +const loadBlog = async () => { + const titles = await loadFolderFrontmatter('title', 'org', 'blog') + const order = await loadFolderFrontmatter('date', 'org', 'blog', formatDate) + + return mergeOrder(titles, order) +} + +/* + * Loads all showcase posts, titles and order + */ +const loadShowcase = async () => { + const titles = await loadFolderFrontmatter('title', 'org', 'blog') + const order = await loadFolderFrontmatter('date', 'org', 'blog') + + return mergeOrder(titles, order) +} + +/* + * Loads all newsletter posts, titles and order + */ +const loadNewsletter = async () => { + const titles = await loadFolderFrontmatter('title', 'org', 'newsletter') + const order = await loadFolderFrontmatter('edition', 'org', 'newsletter') + + return mergeOrder(titles, order) +} + +/* + * Write out prebuild files + */ +const writeFiles = async (type, site, pages) => { + let allPaths = `` + for (const lang in pages) { + fs.writeFileSync( + path.resolve('..', site, 'prebuild', `${type}.${lang}.mjs`), + `${header}export const pages = ${JSON.stringify(pages[lang])}` + ) + allPaths += `import { pages as ${lang} } from './${type}.${lang}.mjs'` + '\n' + } + // Write umbrella file + fs.writeFileSync( + path.resolve('..', site, 'prebuild', `${type}.mjs`), + `${allPaths}${header} + +export const pages = { ${Object.keys(pages).join(',')} }` + ) +} + +/* + * Main method that does what needs doing for the docs + */ +export const prebuildDocs = async (store) => { + store.docs = await loadDocs(store.site) + await writeFiles('docs', store.site, store.docs) +} + +/* + * Main method that does what needs doing for the blog/showcase/newsletter posts + */ +export const prebuildPosts = async (store) => { + store.posts = { + blog: await loadBlog(), + showcase: await loadShowcase(), + newsletter: await loadNewsletter(), + } + await writeFiles('blog', 'org', store.posts.blog) + await writeFiles('showcase', 'org', store.posts.showcase) + await writeFiles('newsletter', 'org', store.posts.newsletter) +} diff --git a/sites/shared/prebuild/navigation.mjs b/sites/shared/prebuild/navigation.mjs index 1c5febb13e8..6c34d747b65 100644 --- a/sites/shared/prebuild/navigation.mjs +++ b/sites/shared/prebuild/navigation.mjs @@ -1,77 +1,106 @@ import path from 'path' import fs from 'fs' import set from 'lodash.set' -import { loadYaml, folders } from './i18n.mjs' +import orderBy from 'lodash.orderby' +//import { loadYaml, folders } from './i18n.mjs' +import { extendSiteNav as dev } from './sitenav-dev.mjs' +import { pageHasChildren } from '../utils.mjs' -// Some arbitrary future time -const future = new Date('10-12-2026').getTime() +const extendNav = { dev } -// We need to load the translation for blog + showcase -const loadTranslation = (locale) => { - let data - try { - data = loadYaml(`${folders.shared[0]}/navigation/sections.${locale}.yaml`, false) - } catch (err) { - data = {} +/* + * A method to recursively add the ordered slugs to the LUT + */ +const flattenOrderedChildPages = (nav) => { + const slugs = [] + for (const page of orderBy(nav, ['o', 't'], ['asc', 'asc'])) { + if (page.s) { + slugs.push(page.s) + if (pageHasChildren(page)) slugs.push(...flattenOrderedChildPages(page)) + } } - if (!data) data = {} - return data + return slugs +} + +/* + * This builds the slugLut (slug look up table) which makes it trivial to + * build the PrevNext component as it builds a flat list of all pages in + * the order they are naturally presented to the reader. So if you have + * a page's slug, you merely need to look it up in the list and return the + * next entry (or previous) + */ +export const orderedSlugLut = (nav) => { + const slugs = [] + for (const page of orderBy(nav, ['o', 't'], ['asc', 'asc'])) { + if (page.s) { + slugs.push(page.s) + if (pageHasChildren(page)) slugs.push(...flattenOrderedChildPages(page)) + } + } + + return slugs } /* * Main method that does what needs doing */ -export const prebuildNavigation = (docPages, sanityPosts, site) => { +export const prebuildNavigation = async (store) => { + const { docs, site, posts = false } = store /* * Since this is written to disk and loaded as JSON, we minimize * the data to load by using the following 1-character keys: * * t: title - * l: link title (shorter version of the title, optional * o: order, optional * s: slug without leading or trailing slash (/) */ - const nav = {} - for (const lang in docPages) { - const translations = loadTranslation(lang) - nav[lang] = {} + const sitenav = {} + const sluglut = {} + for (const lang in docs) { + sitenav[lang] = {} + sluglut[lang] = {} - // Handle MDX content - for (const slug of Object.keys(docPages[lang]).sort()) { - const page = docPages[lang][slug] - const chunks = slug.split('/') + // Handle docs + for (const slug of Object.keys(docs[lang]).sort()) { + const page = docs[lang][slug] const val = { t: page.t, s: slug, } if (page.o) val.o = page.o - set(nav, [lang, ...chunks], val) + set(sitenav, [lang, ...slug.split('/')], val) } - // Handle strapi content - for (const type in sanityPosts) { - set(nav, [lang, type], { - t: translations[type] || type, - l: type, - s: type, - o: type === 'blog' ? 50 : 20, - }) - for (const page of sanityPosts[type][lang]) { - const chunks = page.slug.split('/') - set(nav, [lang, type, ...chunks], { - t: page.title, - l: page.linktitle, - s: type + '/' + page.slug, - o: (future - new Date(page.date).getTime()) / 100000, - }) + // Handle posts + if (posts) { + for (const type in posts) { + for (const [slug, post] of Object.entries(posts[type][lang])) { + set(sitenav, [lang, ...slug.split('/')], { t: post.t, o: post.o, s: slug }) + } } } - } - fs.writeFileSync( - path.resolve('..', site, 'prebuild', `navigation.mjs`), - `export const prebuildNavigation = ${JSON.stringify(nav, null, 2)}` - ) - return true + // Extend navigation if there's a method for that + if (extendNav[site]) sitenav[lang] = extendNav[site](sitenav[lang], lang) + // Create slut lookup table + sluglut[lang] = orderedSlugLut(sitenav[lang]) + + // Write out navigation object + fs.writeFileSync( + path.resolve('..', site, 'prebuild', `navigation.${lang}.mjs`), + `export const siteNav = ${JSON.stringify(sitenav[lang])}` + ) + + // Write out slug lookup table (sluglut) + fs.writeFileSync( + path.resolve('..', site, 'prebuild', `sluglut.${lang}.mjs`), + `export const slugLut = ${JSON.stringify(sluglut[lang])}` + ) + } + + // Update the store + store.navigation = { sitenav, sluglut } + + return } diff --git a/sites/shared/prebuild/og/index.mjs b/sites/shared/prebuild/og/index.mjs index 9c9e6a63333..94b8cb40b3b 100644 --- a/sites/shared/prebuild/og/index.mjs +++ b/sites/shared/prebuild/og/index.mjs @@ -144,7 +144,7 @@ const writeAsPng = async (svg, site, slug) => { * } */ -export const generateOgImage = async (data) => { +export const prebuildOgImages = async (data) => { // Inject into SVG const meta = await getMetaData(data) const svg = decorateSvg(meta) diff --git a/sites/shared/prebuild/patrons.mjs b/sites/shared/prebuild/patrons.mjs index 96309bef3bc..1c2f6690f43 100644 --- a/sites/shared/prebuild/patrons.mjs +++ b/sites/shared/prebuild/patrons.mjs @@ -2,34 +2,53 @@ import path from 'path' import fs from 'fs' import axios from 'axios' -/* - * Main method that does what needs doing - */ -export const prebuildPatrons = async(site) => { - - // Say hi - console.log() - console.log(`Prebuilding patron list for freesewing.${site}`) +export const prebuildPatrons = async (store, mock = false) => { + if (mock) return (store.patrons = mockedData) let patrons try { + // FIXME: Adapt this when the v3 backend is production-ready patrons = await axios.get('https://backend.freesewing.org/patrons') - } - catch (err) { + } catch (err) { console.log(`⚠️ Failed to load patron list`) } const list = patrons?.data ? [ - ...patrons.data['2'].map(p => ({hande: p.handle, username: p.username, img: p.pictureUris.s })), - ...patrons.data['4'].map(p => ({hande: p.handle, username: p.username, img: p.pictureUris.s })), - ...patrons.data['8'].map(p => ({hande: p.handle, username: p.username, img: p.pictureUris.s })), - ] : [] + ...patrons.data['2'].map((p) => ({ + hande: p.handle, + username: p.username, + img: p.pictureUris.s, + })), + ...patrons.data['4'].map((p) => ({ + hande: p.handle, + username: p.username, + img: p.pictureUris.s, + })), + ...patrons.data['8'].map((p) => ({ + hande: p.handle, + username: p.username, + img: p.pictureUris.s, + })), + ] + : [] // Write to json fs.writeFileSync( - path.resolve('..', site, 'prebuild', `patrons.js`), - `export default ${JSON.stringify(list, null ,2)}` + path.resolve('..', store.site, 'prebuild', `patrons.js`), + `export default ${JSON.stringify(list, null, 2)}` ) + + store.patrons = list } +/* + * In development, we return this mocked data to speed things up + */ +const mockedData = [ + { + hande: 'xdpug', + username: 'wouter.vdub', + img: 'https://static.freesewing.org/users/x/xdpug/s-xdpug.jpg', + }, +] diff --git a/sites/shared/prebuild/runner.mjs b/sites/shared/prebuild/runner.mjs new file mode 100644 index 00000000000..fb90a059a17 --- /dev/null +++ b/sites/shared/prebuild/runner.mjs @@ -0,0 +1,136 @@ +// Dependencies +import { oraPromise } from 'ora' +import { capitalize } from '../utils.mjs' +// Handlers +import { prebuildDocs as docs, prebuildPosts as posts } from './markdown.mjs' +import { prebuildNavigation as navigation } from './navigation.mjs' +import { prebuildGitData as git } from './git.mjs' +import { prebuildContributors as contributors } from './contributors.mjs' +import { prebuildPatrons as patrons } from './patrons.mjs' +import { prebuildI18n as i18n } from './i18n.mjs' +//import { prebuildLab as lab} from './lab.mjs' +import { prebuildDesigns as designs } from './designs.mjs' +import { prebuildFavicon as favicon } from './favicon.mjs' +import { prebuildOgImages as ogImages } from './og/index.mjs' +import { prebuildCrowdin as crowdin } from './crowdin.mjs' + +/* + * Are we running in production? + */ +const PRODUCTION = process.env.NODE_ENV === 'production' + +/* + * Structure handlers in a single object + */ +const handlers = { + designs, + contributors, + crowdin, + i18n, + favicon, + patrons, + docs, + posts, + navigation, + git, + // FIXME: This needs work, but perhaps after v3 + //ogImages, +} + +export const prebuildRunner = async ({ + site, // The site we are running the prebuild for + prebuild, // The prebuild configuration object. See sites/[site]/prebuild.mjs +}) => { + /* + * Setup a place where we can keep data + */ + const store = { site } + + /* + * Let the user know what's going to happen + */ + logSummary(site, prebuild) + /* + * To avoid order issues, we use the order as configured + * above, not the order as passed by the prebuild script + */ + for (const step in handlers) { + if (prebuild[step] === true) + await oraPromise(handlers[step](store), { text: `Prebuild ${capitalize(step)}` }) + else if (prebuild[step] === 'productionOnly') + await oraPromise(handlers[step](store, !PRODUCTION), { + text: `Prebuild ${capitalize(step)}${PRODUCTION ? '' : ' (mocked)'}`, + }) + else await oraPromise(() => true, { text: `Prebuild ${capitalize(step)} (skipped)` }) + } + + console.log() + + return +} + +const logSummary = (site, prebuild) => { + console.log() + console.log() + console.log(`👷 Preparing prebuild step for FreeSewing's ${site} site`) + console.log( + `${PRODUCTION ? '🚀' : '🚧'} This ${PRODUCTION ? 'is' : 'is not'} a production build` + ) + console.log(`🏁 We will run the following prebuild steps:`) + console.log() + for (const step in prebuild) { + if (prebuild[step] === 'productionOnly') { + if (PRODUCTION) console.log(`🟢 Prebuild ${capitalize(step)}`) + else console.log(`🟡 Mock ${capitalize(step)}`) + } else if (prebuild[step]) console.log(`🟢 Prebuild ${capitalize(step)}`) + else console.log(`🔴 Skip ${capitalize(step)}`) + } + console.log() + console.log(`👷 Let's get to work...`) + console.log() +} + +const run = async () => { + if (process.env.LINTER) return true + const FAST = process.env.FAST ? true : false + const SITE = process.env.SITE || 'lab' + await prebuildDesigns() + if (['org', 'dev'].includes(SITE)) { + if (!FAST) await prebuildGitData(SITE) + const docs = await prebuildDocs(SITE) + let posts = false + if (SITE === 'org') { + if (!FAST) await prebuildCrowdin() + posts = await prebuildPosts() + } + prebuildNavigation(docs, posts, SITE) + if (!FAST && process.env.GENERATE_OG_IMAGES) { + // Create og image for the home page + await generateOgImage({ + lang: 'en', + site: SITE, + slug: '', + title: 'FreeSewing.dev', + }) + // Create og image for the 404 page + await generateOgImage({ + lang: 'en', + site: SITE, + slug: '/404', + intro: "There's nothing here. Only this message to say there's nothing here.", + title: 'Page not found', + lead: '404', + }) + } + } else { + await prebuildLab() + } + + await prebuildI18n(SITE) + if (!FAST) { + await prebuildContributors(SITE) + await prebuildPatrons(SITE) + await prebuildFavicon(SITE) + } + console.log() +} diff --git a/sites/shared/prebuild/sitenav-dev.mjs b/sites/shared/prebuild/sitenav-dev.mjs new file mode 100644 index 00000000000..76aa4c0c82c --- /dev/null +++ b/sites/shared/prebuild/sitenav-dev.mjs @@ -0,0 +1,54 @@ +/* Remember Mc_Shifton: + * Note: Set 'm' to truthy to show this as a main section in the side-navigation (optional) + * Note: Set 'c' to set the control level to hide things from users (optional) + * Note: Set 's' to the slug (optional insofar as it's not a real page (a spacer for the header)) + * Note: Set '_' to never show the page in the site navigation (like the tags pages) + * Note: Set 'h' to indicate this is a top-level page that should be hidden from the side-nav (like search) + * Note: Set 'i' when something should be included as top-level in the collapse side-navigation (optional) + * Note: Set 'f' to add the page to the footer + * Note: Set 't' to the title + * Note: Set 'o' to set the order (optional) + * Note: Set 'n' to mark this as a noisy entry that should always be closed unless active (like blog) + */ + +export const extendSiteNav = (pages, lang) => { + pages.about = { + s: 'about', + t: 'About FreeSewing', + } + + let order = 10 + for (const slug of ['api', 'design', 'contribute', 'i18n', 'infra', 'about', 'support']) { + pages[slug].m = 1 + pages[slug].o = order + order += 10 + } + + pages.search = { + s: 'search', + h: 1, + f: 1, + t: 'Search', + o: 270, + } + pages.sitemap = { + s: 'sitemap', + h: 1, + f: 1, + t: 'Sitemap', + o: 270, + } + + // Make top-level documentation entries appear in i-list + order = 10 + for (const slug of ['tutorials', 'guides', 'howtos', 'reference', 'training']) { + pages[slug].o = order + pages[slug].i = 1 + order += 10 + } + + // Hide contact from the sitenav + pages.contact.h = 1 + + return pages +} diff --git a/sites/shared/utils.mjs b/sites/shared/utils.mjs index e86027e920a..f6a921690da 100644 --- a/sites/shared/utils.mjs +++ b/sites/shared/utils.mjs @@ -279,6 +279,17 @@ export const shortDate = (locale = 'en', timestamp = false) => { return ts.toLocaleDateString(locale, options) } +export const yyyymmdd = (timestamp = false) => { + const ts = timestamp ? new Date(timestamp) : new Date() + + let m = String(ts.getMonth() + 1) + if (m.length === 1) m = '0' + m + let d = '' + ts.getDate() + if (d.length === 1) d = '0' + d + + return `${ts.getFullYear()}${m}${d}` +} + export const scrollTo = (id) => { // eslint-disable-next-line no-undef const el = document ? document.getElementById(id) : null diff --git a/yarn.lock b/yarn.lock index 697242288d0..7da18d5a3f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14893,6 +14893,21 @@ ora@^5.1.0, ora@^5.4.1: strip-ansi "^6.0.0" wcwidth "^1.0.1" +ora@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-6.3.1.tgz#a4e9e5c2cf5ee73c259e8b410273e706a2ad3ed6" + integrity sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ== + dependencies: + chalk "^5.0.0" + cli-cursor "^4.0.0" + cli-spinners "^2.6.1" + is-interactive "^2.0.0" + is-unicode-supported "^1.1.0" + log-symbols "^5.1.0" + stdin-discarder "^0.1.0" + strip-ansi "^7.0.1" + wcwidth "^1.0.1" + org-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/org-regex/-/org-regex-1.0.0.tgz#67ebb9ab3cb124fea5841289d60b59434f041a59" @@ -18567,6 +18582,13 @@ statuses@~1.4.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== +stdin-discarder@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.1.0.tgz#22b3e400393a8e28ebf53f9958f3880622efde21" + integrity sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ== + dependencies: + bl "^5.0.0" + stop-iteration-iterator@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" From 81e111bcca028d613666fb15c614d73c5f910530 Mon Sep 17 00:00:00 2001 From: Joost De Cock Date: Thu, 20 Jul 2023 08:39:45 +0200 Subject: [PATCH 02/12] fix: Add gap to layout --- sites/shared/components/base-layout.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sites/shared/components/base-layout.mjs b/sites/shared/components/base-layout.mjs index 1edbea306f9..c7db16e48fe 100644 --- a/sites/shared/components/base-layout.mjs +++ b/sites/shared/components/base-layout.mjs @@ -2,7 +2,7 @@ * The default full-page FreeSewing layout */ export const BaseLayout = ({ children = [] }) => ( -
+
{children}
) From 595cae3ebf56f0d342ddbff006c4fc7bc049972a Mon Sep 17 00:00:00 2001 From: Joost De Cock Date: Thu, 20 Jul 2023 08:41:49 +0200 Subject: [PATCH 03/12] fix(markdown): Incorrectly nested HTML --- markdown/dev/tutorials/getting-started-codespaces/en.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/markdown/dev/tutorials/getting-started-codespaces/en.md b/markdown/dev/tutorials/getting-started-codespaces/en.md index 55fa8f33d48..5a6a8d9cb26 100644 --- a/markdown/dev/tutorials/getting-started-codespaces/en.md +++ b/markdown/dev/tutorials/getting-started-codespaces/en.md @@ -108,7 +108,8 @@ After the lab, dev, or org website starts: custom URL. - You can also access the custom URL via the "Ports" panel. - + + An example of a custom URL: `https://username-ominous-space-waffle-rwpgzw5q15vqc52q9-8000.preview.app.github.dev/` From 78fa37869e20c4d3c1bac50f504a0a9b68df1546 Mon Sep 17 00:00:00 2001 From: Joost De Cock Date: Thu, 20 Jul 2023 08:42:56 +0200 Subject: [PATCH 04/12] chore(shared/dev): Remove use-navigation hook --- .../dev/components/navigation/modal-menu.mjs | 7 +- .../components/navigation/sections-menu.mjs | 5 +- sites/dev/hooks/use-navigation.mjs | 4 - sites/dev/pages/404.mjs | 24 +- sites/dev/pages/[...slug].mjs | 12 +- sites/dev/pages/about.mjs | 15 +- sites/dev/pages/search.mjs | 24 +- sites/shared/components/designs/design.mjs | 4 +- sites/shared/components/footer/index.mjs | 6 +- sites/shared/components/mdx/docs-helpers.mjs | 5 +- sites/shared/components/mdx/meta.mjs | 1 - sites/shared/components/mdx/read-more.mjs | 4 +- .../shared/components/navigation/sitenav.mjs | 47 +-- sites/shared/components/prev-next.mjs | 9 +- sites/shared/components/sets/set.mjs | 4 +- sites/shared/components/wrappers/page.mjs | 25 -- sites/shared/config/designs.mjs | 6 +- sites/shared/config/freesewing.config.mjs | 2 +- sites/shared/context/modal-context.mjs | 4 +- sites/shared/context/navigation-context.mjs | 151 +++++----- sites/shared/i18n/tags.en.yaml | 2 + sites/shared/prebuild/docs.mjs | 3 +- sites/shared/prebuild/git.mjs | 4 +- sites/shared/prebuild/navigation.mjs | 46 ++- sites/shared/prebuild/sitenav-org.mjs | 270 ++++++++++++++++++ 25 files changed, 485 insertions(+), 199 deletions(-) delete mode 100644 sites/dev/hooks/use-navigation.mjs create mode 100644 sites/shared/prebuild/sitenav-org.mjs diff --git a/sites/dev/components/navigation/modal-menu.mjs b/sites/dev/components/navigation/modal-menu.mjs index 9d4dc4dea65..f83ca281cc2 100644 --- a/sites/dev/components/navigation/modal-menu.mjs +++ b/sites/dev/components/navigation/modal-menu.mjs @@ -1,5 +1,7 @@ +// Dependencies +import { NavigationContext } from 'shared/context/navigation-context.mjs' // Hooks -import { useNavigation } from 'site/hooks/use-navigation.mjs' +import { useContext } from 'react' // Components import { SectionsMenu, ns as sectionsNs } from 'site/components/navigation/sections-menu.mjs' import { ModalWrapper } from 'shared/components/wrappers/modal.mjs' @@ -11,7 +13,8 @@ import { NavLinks, Breadcrumbs } from 'shared/components/navigation/sitenav.mjs' export const ns = nsMerge(sectionsNs) export const ModalMenu = ({ slug }) => { - const { siteNav } = useNavigation() + // Grab siteNav from the navigation context + const { siteNav } = useContext(NavigationContext) return ( diff --git a/sites/dev/components/navigation/sections-menu.mjs b/sites/dev/components/navigation/sections-menu.mjs index 79badb22fe7..5d909d94d16 100644 --- a/sites/dev/components/navigation/sections-menu.mjs +++ b/sites/dev/components/navigation/sections-menu.mjs @@ -1,9 +1,10 @@ +import { useContext } from 'react' +import { NavigationContext } from 'shared/context/navigation-context.mjs' import Link from 'next/link' import { icons, ns as sectionsNs } from 'shared/components/navigation/primary.mjs' import { useTranslation } from 'next-i18next' import orderBy from 'lodash.orderby' import { colors } from 'shared/components/header.mjs' -import { useNavigation } from 'site/hooks/use-navigation.mjs' export const ns = sectionsNs @@ -11,7 +12,7 @@ const onlySections = (tree) => orderBy(tree, ['t'], ['asc']).filter((entry) => e export const SectionsMenu = ({ bOnly = false }) => { const { t } = useTranslation(ns) - const { siteNav } = useNavigation() + const { siteNav } = useContext(NavigationContext) const output = [] let i = 1 diff --git a/sites/dev/hooks/use-navigation.mjs b/sites/dev/hooks/use-navigation.mjs deleted file mode 100644 index 4d1fae9f676..00000000000 --- a/sites/dev/hooks/use-navigation.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import { siteNav } from 'site/prebuild/navigation.en.mjs' -import { slugLut } from 'site/prebuild/sluglut.en.mjs' - -export const useNavigation = () => ({ siteNav, slugLut }) diff --git a/sites/dev/pages/404.mjs b/sites/dev/pages/404.mjs index a331e200e76..113986a7e63 100644 --- a/sites/dev/pages/404.mjs +++ b/sites/dev/pages/404.mjs @@ -1,17 +1,18 @@ -// Hooks -import { useNavigation } from 'site/hooks/use-navigation.mjs' +// Dependencies +import { serverSideTranslations } from 'next-i18next/serverSideTranslations' // Components import Head from 'next/head' -import { PageWrapper } from 'shared/components/wrappers/page.mjs' +import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs' import { Robot } from 'shared/components/robot/index.mjs' import { Popout } from 'shared/components/popout.mjs' import { PageLink } from 'shared/components/page-link.mjs' import { BaseLayout, BaseLayoutLeft, BaseLayoutWide } from 'shared/components/base-layout.mjs' import { NavLinks, MainSections } from 'shared/components/navigation/sitenav.mjs' +const namespaces = [...pageNs] + const Page404 = () => { const title = '404: Page not found' - const { siteNav } = useNavigation({ ignoreControl: true }) const slug = '404' return ( @@ -34,8 +35,8 @@ const Page404 = () => { - - + +
@@ -62,3 +63,14 @@ const Page404 = () => { } export default Page404 + +export async function getStaticProps() { + return { + props: { + ...(await serverSideTranslations('en', namespaces)), + page: { + path: ['search'], + }, + }, + } +} diff --git a/sites/dev/pages/[...slug].mjs b/sites/dev/pages/[...slug].mjs index 357ec4e1915..dbe61634675 100644 --- a/sites/dev/pages/[...slug].mjs +++ b/sites/dev/pages/[...slug].mjs @@ -4,7 +4,6 @@ import { pages } from 'site/prebuild/docs.en.mjs' import { serverSideTranslations } from 'next-i18next/serverSideTranslations' // Hooks import { useState, useEffect } from 'react' -import { useNavigation } from 'site/hooks/use-navigation.mjs' // Components import Head from 'next/head' import { PageWrapper, ns } from 'shared/components/wrappers/page.mjs' @@ -31,11 +30,6 @@ import { const DocsPage = ({ page, slug }) => { const [frontmatter, setFrontmatter] = useState({ title: 'FreeSewing.dev' }) const [MDX, setMDX] = useState() - /* - * Get the siteNav object from the useNavigation hook - * FIXME: ignorecontrol is not yet implmented here - */ - const { siteNav } = useNavigation({ ignoreControl: true }) /* Load MDX dynamically */ useEffect(() => { @@ -71,12 +65,12 @@ const DocsPage = ({ page, slug }) => { - - + +
- +

{frontmatter.title}

diff --git a/sites/dev/pages/about.mjs b/sites/dev/pages/about.mjs index ad569c4afbf..0f7ce0778c5 100644 --- a/sites/dev/pages/about.mjs +++ b/sites/dev/pages/about.mjs @@ -1,8 +1,6 @@ // Dependencies import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { freeSewingConfig } from 'shared/config/freesewing.config.mjs' -// Hooks -import { useNavigation } from 'site/hooks/use-navigation.mjs' // Components import Head from 'next/head' import { PageWrapper } from 'shared/components/wrappers/page.mjs' @@ -18,12 +16,7 @@ import { } from 'shared/components/base-layout.mjs' const ContactPage = ({ page, slug }) => { - /* - * Get the siteNav object from the useNavigation hook - * FIXME: ignorecontrol is not yet implmented here - */ - const { siteNav } = useNavigation({ ignoreControl: true }) - const title = siteNav.about.t + const title = 'About FreeSewing' return ( @@ -45,12 +38,12 @@ const ContactPage = ({ page, slug }) => { - - + +
- +

{title}

diff --git a/sites/dev/pages/search.mjs b/sites/dev/pages/search.mjs index 693ffcda751..a7875baac9d 100644 --- a/sites/dev/pages/search.mjs +++ b/sites/dev/pages/search.mjs @@ -1,9 +1,7 @@ // Dependencies import { serverSideTranslations } from 'next-i18next/serverSideTranslations' -// Hooks -import { useNavigation } from 'site/hooks/use-navigation.mjs' // Components -import { PageWrapper } from 'shared/components/wrappers/page.mjs' +import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs' import { Search } from 'site/components/search.mjs' import { Popout } from 'shared/components/popout.mjs' import { PageLink } from 'shared/components/page-link.mjs' @@ -15,14 +13,12 @@ import { BaseLayoutRight, } from 'shared/components/base-layout.mjs' -const SearchPage = ({ page, slug }) => { - /* - * Get the siteNav object from the useNavigation hook - * FIXME: ignorecontrol is not yet implmented here - */ - const { siteNav } = useNavigation({ ignoreControl: true }) - const title = siteNav.about.t +const namespaces = [...pageNs] +const SearchPage = ({ page, slug }) => { + const title = 'Search' + + console.log(namespaces) const tip = ( The can also be helpful to find things. @@ -33,12 +29,12 @@ const SearchPage = ({ page, slug }) => { - - + +
- +

{title}

{tip}
@@ -55,7 +51,7 @@ export default SearchPage export async function getStaticProps() { return { props: { - ...(await serverSideTranslations('en')), + ...(await serverSideTranslations('en', namespaces)), slug: 'search', page: { path: ['search'], diff --git a/sites/shared/components/designs/design.mjs b/sites/shared/components/designs/design.mjs index de05ba7eee5..58c86414f4f 100644 --- a/sites/shared/components/designs/design.mjs +++ b/sites/shared/components/designs/design.mjs @@ -4,7 +4,7 @@ import { Difficulty } from 'shared/components/designs/difficulty.mjs' import { designs } from 'shared/config/designs.mjs' import { DesignTag } from 'shared/components/designs/tag.mjs' -export const ns = ['design', 'designs', 'tags'] +export const ns = ['designs', 'tags'] const defaultLink = (design) => `/new/${design}` @@ -27,7 +27,7 @@ export const Design = ({ name, hrefBuilder = false }) => {
{t(`designs:${name}.t`)} - {t('design:difficulty')} + {t('tags:difficulty')}
diff --git a/sites/shared/components/footer/index.mjs b/sites/shared/components/footer/index.mjs index 6c3a13a5152..157e7f894b9 100644 --- a/sites/shared/components/footer/index.mjs +++ b/sites/shared/components/footer/index.mjs @@ -1,7 +1,8 @@ // Dependencies import orderBy from 'lodash.orderby' +import { NavigationContext } from 'shared/context/navigation-context.mjs' // Hooks -import { useNavigation } from 'site/hooks/use-navigation.mjs' +import { useContext } from 'react' // Components import Link from 'next/link' import { Ribbon } from 'shared/components/ribbon.mjs' @@ -14,7 +15,8 @@ export const ns = ['footer', ...sponsorsNs] const onlyFooterLinks = (tree) => orderBy(tree, ['t'], ['asc']).filter((entry) => entry.f) export const Footer = () => { - const { siteNav } = useNavigation({ ignoreControl: true }) + // Grab siteNav from the navigation context + const { siteNav } = useContext(NavigationContext) return (