Merge pull request #4524 from freesewing/joost
feat(sites): Refactor prebuild step - Remove useNavigation hook
This commit is contained in:
commit
15078438a6
62 changed files with 1574 additions and 1110 deletions
|
@ -5,6 +5,8 @@
|
|||
*/
|
||||
export const authors = {
|
||||
joostdecock: { id: 0, name: 'Joost De Cock' },
|
||||
'Prof. dr. Sorcha Ní Dhubhghaill': { id: 0, name: 'Prof. dr. Sorcha Ní Dhubhghaill' },
|
||||
mocked: { id: 0, name: 'Unknown (mocked in dev)' },
|
||||
benjamesben: { id: 0, name: 'Benjamin' },
|
||||
nikhil: { id: 0, name: 'nikhil' },
|
||||
jackseye: { id: 0, name: 'jackseye' },
|
||||
|
@ -30,8 +32,7 @@ export const authors = {
|
|||
Natalia: { id: 0, name: 'Natalia Sayang' },
|
||||
chri5b: { id: 0, name: 'chri5b' },
|
||||
tangerineshark: { id: 0, name: 'tangerineshark' },
|
||||
'MA-TATAS': { id: 0, name: 'MA-TATAS' },
|
||||
'Ivo Bek': { id: 0, name: 'Ivo Bek' },
|
||||
'bekivo@gmail.com': { id: 0, name: 'Ivo Bek' },
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -47,4 +48,5 @@ export const gitToAuthor = {
|
|||
'bobgeorgethe3rd@googlemail.com': 'bobgeorgethe3rd',
|
||||
'70777269+tangerineshark@users.noreply.github.com': 'tangerineshark',
|
||||
'thijs.assies@gmail.com': 'MA-TATAS',
|
||||
'Natalia Sayang': 'Natalia',
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
||||
<Tip compact>
|
||||
<Tip>
|
||||
|
||||
An example of a custom URL: `https://username-ominous-space-waffle-rwpgzw5q15vqc52q9-8000.preview.app.github.dev/`
|
||||
</Tip>
|
||||
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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 (
|
||||
<ModalWrapper flex="col" justify="top" slideFrom="left">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
import { prebuildNavigation as pbn } from 'site/prebuild/navigation.mjs'
|
||||
import { orderedSlugLut } from 'shared/hooks/use-navigation-helpers.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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -1,64 +1,71 @@
|
|||
// 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 Page404 = () => {
|
||||
const title = '404: Page not found'
|
||||
const { siteNav } = useNavigation({ ignoreControl: true })
|
||||
const slug = '404'
|
||||
const namespaces = [...pageNs]
|
||||
|
||||
return (
|
||||
<PageWrapper title={title}>
|
||||
<Head>
|
||||
<meta property="og:type" content="article" key="type" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="There's nothing here. If you followed a link to get here, that link is broken"
|
||||
key="description"
|
||||
/>
|
||||
<meta property="og:article:author" content="Joost De Cock" key="author" />
|
||||
<meta property="og:image" content={`https://freesewing.dev/og/404/og.png`} key="image" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:url" content={`https://freesewing.dev/`} key="url" />
|
||||
<meta property="og:locale" content="en_US" key="locale" />
|
||||
<meta property="og:site_name" content="freesewing.dev" key="site" />
|
||||
</Head>
|
||||
<BaseLayout>
|
||||
<BaseLayoutLeft>
|
||||
<MainSections {...{ siteNav, slug }} />
|
||||
<NavLinks {...{ siteNav, slug }} />
|
||||
</BaseLayoutLeft>
|
||||
<BaseLayoutWide>
|
||||
<div className="max-w-2xl">
|
||||
<h1>404: Page not found</h1>
|
||||
<div className="max-w-sm m-auto px-12 mb-4">
|
||||
<Robot embed pose="fail" />
|
||||
</div>
|
||||
<h3>We could not find what you are looking for</h3>
|
||||
<div className="text-left">
|
||||
<Popout comment by="joost">
|
||||
<h5>Did you arrive here from a link?</h5>
|
||||
<p>In that case, that link is broken.</p>
|
||||
<p>
|
||||
If it was one of our links, please <PageLink href="/contact" txt="let us know" />{' '}
|
||||
so we can fix it.
|
||||
</p>
|
||||
</Popout>
|
||||
</div>
|
||||
const Page404 = () => (
|
||||
<PageWrapper title="404: Page not found">
|
||||
<Head>
|
||||
<meta property="og:type" content="article" key="type" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="There's nothing here. If you followed a link to get here, that link is broken"
|
||||
key="description"
|
||||
/>
|
||||
<meta property="og:article:author" content="Joost De Cock" key="author" />
|
||||
<meta property="og:image" content={`https://freesewing.dev/og/404/og.png`} key="image" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:url" content={`https://freesewing.dev/`} key="url" />
|
||||
<meta property="og:locale" content="en_US" key="locale" />
|
||||
<meta property="og:site_name" content="freesewing.dev" key="site" />
|
||||
</Head>
|
||||
<BaseLayout>
|
||||
<BaseLayoutLeft>
|
||||
<MainSections />
|
||||
<NavLinks />
|
||||
</BaseLayoutLeft>
|
||||
<BaseLayoutWide>
|
||||
<div className="max-w-2xl">
|
||||
<h1>404: Page not found</h1>
|
||||
<div className="max-w-sm m-auto px-12 mb-4">
|
||||
<Robot embed pose="fail" />
|
||||
</div>
|
||||
</BaseLayoutWide>
|
||||
</BaseLayout>
|
||||
</PageWrapper>
|
||||
)
|
||||
}
|
||||
<h3>We could not find what you are looking for</h3>
|
||||
<div className="text-left">
|
||||
<Popout comment by="joost">
|
||||
<h5>Did you arrive here from a link?</h5>
|
||||
<p>In that case, that link is broken.</p>
|
||||
<p>
|
||||
If it was one of our links, please <PageLink href="/contact" txt="let us know" /> so
|
||||
we can fix it.
|
||||
</p>
|
||||
</Popout>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayoutWide>
|
||||
</BaseLayout>
|
||||
</PageWrapper>
|
||||
)
|
||||
|
||||
export default Page404
|
||||
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations('en', namespaces)),
|
||||
page: {
|
||||
path: ['404'],
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
// 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
|
||||
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(<Spinner />)
|
||||
/*
|
||||
* 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 }) => {
|
|||
</Head>
|
||||
<BaseLayout>
|
||||
<BaseLayoutLeft>
|
||||
<MainSections {...{ siteNav, slug }} />
|
||||
<NavLinks {...{ siteNav, slug }} />
|
||||
<MainSections />
|
||||
<NavLinks />
|
||||
</BaseLayoutLeft>
|
||||
<BaseLayoutProse>
|
||||
<div className="w-full">
|
||||
<Breadcrumbs {...{ siteNav, slug }} />
|
||||
<Breadcrumbs />
|
||||
<h1 className="break-words searchme">{frontmatter.title}</h1>
|
||||
<div className="block xl:hidden">
|
||||
<Toc toc={frontmatter.toc} wrap />
|
||||
|
@ -126,7 +120,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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
@ -17,13 +15,8 @@ import {
|
|||
BaseLayoutRight,
|
||||
} 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 ContactPage = ({ page }) => {
|
||||
const title = 'About FreeSewing'
|
||||
|
||||
return (
|
||||
<PageWrapper {...page}>
|
||||
|
@ -45,12 +38,12 @@ const ContactPage = ({ page, slug }) => {
|
|||
</Head>
|
||||
<BaseLayout>
|
||||
<BaseLayoutLeft>
|
||||
<MainSections {...{ siteNav, slug }} />
|
||||
<NavLinks {...{ siteNav, slug }} />
|
||||
<MainSections />
|
||||
<NavLinks />
|
||||
</BaseLayoutLeft>
|
||||
<BaseLayoutProse>
|
||||
<div className="w-full">
|
||||
<Breadcrumbs {...{ siteNav, slug }} />
|
||||
<Breadcrumbs />
|
||||
<h1 className="break-words searchme">{title}</h1>
|
||||
</div>
|
||||
<div className="mdx max-w-prose">
|
||||
|
@ -187,7 +180,6 @@ export async function getStaticProps() {
|
|||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations('en')),
|
||||
slug: 'about',
|
||||
page: {
|
||||
path: ['about'],
|
||||
},
|
||||
|
|
|
@ -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,13 +13,10 @@ 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 }) => {
|
||||
const title = 'Search'
|
||||
|
||||
const tip = (
|
||||
<Popout tip compact>
|
||||
|
@ -33,12 +28,12 @@ const SearchPage = ({ page, slug }) => {
|
|||
<PageWrapper {...page}>
|
||||
<BaseLayout>
|
||||
<BaseLayoutLeft>
|
||||
<MainSections {...{ siteNav, slug }} />
|
||||
<NavLinks {...{ siteNav, slug }} />
|
||||
<MainSections />
|
||||
<NavLinks />
|
||||
</BaseLayoutLeft>
|
||||
<BaseLayoutProse>
|
||||
<div className="w-full">
|
||||
<Breadcrumbs {...{ siteNav, slug }} />
|
||||
<Breadcrumbs />
|
||||
<h1 className="break-words searchme">{title}</h1>
|
||||
<div className="block xl:hidden">{tip}</div>
|
||||
</div>
|
||||
|
@ -55,8 +50,7 @@ export default SearchPage
|
|||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations('en')),
|
||||
slug: 'search',
|
||||
...(await serverSideTranslations('en', namespaces)),
|
||||
page: {
|
||||
path: ['search'],
|
||||
},
|
||||
|
|
93
sites/dev/prebuild.mjs
Normal file
93
sites/dev/prebuild.mjs
Normal file
|
@ -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 no such posts on FreeSewing.dev
|
||||
* We could have leave this step out, but it's included here for documenation purposes
|
||||
*/
|
||||
posts: false,
|
||||
},
|
||||
})
|
|
@ -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": {},
|
||||
|
|
|
@ -1,36 +1,24 @@
|
|||
// Hooks
|
||||
import { useNavigation } from 'site/hooks/use-navigation.mjs'
|
||||
// Components
|
||||
import { BaseLayout, BaseLayoutLeft, BaseLayoutWide } from 'shared/components/base-layout.mjs'
|
||||
import { NavLinks, Breadcrumbs, MainSections } from 'shared/components/navigation/sitenav.mjs'
|
||||
|
||||
export const ns = [] //navNs
|
||||
export const ns = []
|
||||
|
||||
export const DefaultLayout = ({ children = [], slug, pageTitle = false }) => {
|
||||
const { siteNav } = useNavigation({ ignoreControl: true })
|
||||
export const DefaultLayout = ({ children = [], pageTitle = false }) => (
|
||||
<BaseLayout>
|
||||
<BaseLayoutLeft>
|
||||
<MainSections />
|
||||
<NavLinks />
|
||||
</BaseLayoutLeft>
|
||||
|
||||
return (
|
||||
<BaseLayout>
|
||||
<BaseLayoutLeft>
|
||||
{slug ? (
|
||||
<>
|
||||
<MainSections {...{ siteNav, slug }} />
|
||||
<NavLinks {...{ siteNav, slug }} />
|
||||
</>
|
||||
) : (
|
||||
<p>Slug not passed to layout</p>
|
||||
)}
|
||||
</BaseLayoutLeft>
|
||||
|
||||
<BaseLayoutWide>
|
||||
{pageTitle && (
|
||||
<div className="xl:pl-4">
|
||||
{slug && <Breadcrumbs {...{ siteNav, slug }} />}
|
||||
<h1 className="break-words">{pageTitle}</h1>
|
||||
</div>
|
||||
)}
|
||||
<div className="xl:pl-4">{children}</div>
|
||||
</BaseLayoutWide>
|
||||
</BaseLayout>
|
||||
)
|
||||
}
|
||||
<BaseLayoutWide>
|
||||
{pageTitle && (
|
||||
<div className="xl:pl-4">
|
||||
<Breadcrumbs />
|
||||
<h1 className="break-words">{pageTitle}</h1>
|
||||
</div>
|
||||
)}
|
||||
<div className="xl:pl-4">{children}</div>
|
||||
</BaseLayoutWide>
|
||||
</BaseLayout>
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { NavigationContext } from 'shared/context/navigation-context.mjs'
|
||||
// Hooks
|
||||
import { useNavigation } from 'site/hooks/use-navigation.mjs'
|
||||
import { useContext } from 'react'
|
||||
// Components
|
||||
import Head from 'next/head'
|
||||
import {
|
||||
|
@ -45,21 +46,21 @@ export const FrontmatterHead = ({ frontmatter, slug, locale }) => (
|
|||
</Head>
|
||||
)
|
||||
|
||||
export const DocsLayout = ({ children = [], slug, frontmatter, locale }) => {
|
||||
const { siteNav } = useNavigation({ ignoreControl: true })
|
||||
export const DocsLayout = ({ children = [], frontmatter }) => {
|
||||
const { slug, locale } = useContext(NavigationContext)
|
||||
|
||||
return (
|
||||
<>
|
||||
<FrontmatterHead {...{ frontmatter, slug, locale }} />
|
||||
<BaseLayout>
|
||||
<BaseLayoutLeft>
|
||||
<MainSections {...{ siteNav, slug }} />
|
||||
<NavLinks {...{ siteNav, slug }} />
|
||||
<MainSections />
|
||||
<NavLinks />
|
||||
</BaseLayoutLeft>
|
||||
|
||||
<BaseLayoutProse>
|
||||
<div className="w-full">
|
||||
<Breadcrumbs {...{ siteNav, slug }} />
|
||||
<Breadcrumbs />
|
||||
<h1 className="break-words searchme">{frontmatter.title}</h1>
|
||||
<div className="block xl:hidden">
|
||||
<Toc toc={frontmatter.toc} wrap />
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// Hooks
|
||||
import { useNavigation } from 'site/hooks/use-navigation.mjs'
|
||||
// Components
|
||||
import { FrontmatterHead } from './docs.mjs'
|
||||
import {
|
||||
|
@ -17,40 +15,36 @@ import {
|
|||
import { Toc } from 'shared/components/mdx/toc.mjs'
|
||||
import { PrevNext } from 'shared/components/prev-next.mjs'
|
||||
|
||||
export const ns = [navNs, 'docs'] //navNs
|
||||
export const ns = [navNs, 'docs']
|
||||
|
||||
const isEndSlug = (slug) => slug.split('/').length === 1
|
||||
|
||||
export const PostLayout = ({ children = [], slug, frontmatter, locale }) => {
|
||||
const { siteNav } = useNavigation({ ignoreControl: true })
|
||||
export const PostLayout = ({ children = [], slug, frontmatter, locale }) => (
|
||||
<>
|
||||
<FrontmatterHead {...{ frontmatter, slug, locale }} />
|
||||
<BaseLayout>
|
||||
<BaseLayoutLeft>
|
||||
<MainSections />
|
||||
<NavLinks />
|
||||
</BaseLayoutLeft>
|
||||
|
||||
return (
|
||||
<>
|
||||
<FrontmatterHead {...{ frontmatter, slug, locale }} />
|
||||
<BaseLayout>
|
||||
<BaseLayoutLeft>
|
||||
<MainSections {...{ siteNav, slug }} />
|
||||
<NavLinks {...{ siteNav, slug }} />
|
||||
</BaseLayoutLeft>
|
||||
|
||||
<BaseLayoutProse>
|
||||
<div className="w-full">
|
||||
<Breadcrumbs {...{ siteNav, slug }} />
|
||||
<h1 className="break-words searchme">{frontmatter.title}</h1>
|
||||
<div className="block xl:hidden">
|
||||
<Toc toc={frontmatter.toc} wrap />
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
<PrevNext slug={slug} noPrev={isEndSlug} noNext={isEndSlug} />
|
||||
</BaseLayoutProse>
|
||||
|
||||
<BaseLayoutRight>
|
||||
<div className="hidden xl:block">
|
||||
<BaseLayoutProse>
|
||||
<div className="w-full">
|
||||
<Breadcrumbs />
|
||||
<h1 className="break-words searchme">{frontmatter.title}</h1>
|
||||
<div className="block xl:hidden">
|
||||
<Toc toc={frontmatter.toc} wrap />
|
||||
</div>
|
||||
</BaseLayoutRight>
|
||||
</BaseLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
{children}
|
||||
<PrevNext noPrev={isEndSlug} noNext={isEndSlug} />
|
||||
</BaseLayoutProse>
|
||||
|
||||
<BaseLayoutRight>
|
||||
<div className="hidden xl:block">
|
||||
<Toc toc={frontmatter.toc} wrap />
|
||||
</div>
|
||||
</BaseLayoutRight>
|
||||
</BaseLayout>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
import { localePath } from 'shared/utils.mjs'
|
||||
const preGenerate = 6
|
||||
export const numPerPage = 12
|
||||
import { siteConfig as config } from 'site/site.config.mjs'
|
||||
|
||||
export const getPostSlugPaths = (order) => {
|
||||
export const getPostSlugPaths = (posts) => {
|
||||
const paths = []
|
||||
|
||||
for (const lang in order) {
|
||||
for (let i = 0; i < preGenerate; i++) {
|
||||
paths.push(localePath(lang, `${order[lang][i]}`))
|
||||
}
|
||||
for (const lang in posts) {
|
||||
paths.push(
|
||||
...Object.keys(posts[lang])
|
||||
.slice(0, config.posts.preGenerate)
|
||||
.map((slug) => localePath(lang, slug))
|
||||
)
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
export const getPostIndexPaths = (order, type) => {
|
||||
export const getPostIndexPaths = (posts, type) => {
|
||||
const paths = []
|
||||
for (const language in order) {
|
||||
for (const language in posts) {
|
||||
paths.push(localePath(language, `${type}/page/1`))
|
||||
paths.push(localePath(language, `${type}/page/2`))
|
||||
}
|
||||
|
@ -24,13 +25,18 @@ export const getPostIndexPaths = (order, type) => {
|
|||
return paths
|
||||
}
|
||||
|
||||
export const getPostIndexProps = (locale, params, order, postInfo) => {
|
||||
const pageNum = parseInt(params.page)
|
||||
const numLocPages = Math.ceil(order[locale].length / numPerPage)
|
||||
export const getPostIndexProps = (pagenr, posts, meta) => {
|
||||
const pageNum = parseInt(pagenr)
|
||||
const numLocPages = Math.ceil(Object.keys(posts).length / config.posts.perPage)
|
||||
if (pageNum > numLocPages) return false
|
||||
|
||||
const postSlugs = order[locale].slice(numPerPage * (pageNum - 1), numPerPage * pageNum)
|
||||
const posts = postSlugs.map((s) => ({ ...postInfo[locale][s], s }))
|
||||
const pagePosts = Object.entries(posts)
|
||||
.slice(config.posts.perPage * (pageNum - 1), config.posts.perPage * pageNum)
|
||||
.map(([slug, post]) => ({
|
||||
s: slug,
|
||||
...post,
|
||||
...meta[slug],
|
||||
}))
|
||||
|
||||
return { posts, current: pageNum, total: numLocPages }
|
||||
return { posts: pagePosts, current: pageNum, total: numLocPages }
|
||||
}
|
||||
|
|
|
@ -1,219 +0,0 @@
|
|||
import { prebuildNavigation as pbn } from 'site/prebuild/navigation.mjs'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { freeSewingConfig as conf } from 'shared/config/freesewing.config.mjs'
|
||||
import { useAccount } from 'shared/hooks/use-account.mjs'
|
||||
import { designs, tags } from 'shared/config/designs.mjs'
|
||||
import { objUpdate } from 'shared/utils.mjs'
|
||||
import { orderedSlugLut } from 'shared/hooks/use-navigation-helpers.mjs'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
/*
|
||||
* 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', 'designs']
|
||||
|
||||
const sitePages = (t = false, control = 99) => {
|
||||
// Handle t not being present
|
||||
if (!t) t = (string) => string
|
||||
const pages = {
|
||||
// Top-level pages that are the sections menu
|
||||
designs: {
|
||||
m: 1,
|
||||
s: 'designs',
|
||||
t: t('sections:designs'),
|
||||
n: 1,
|
||||
tags: {
|
||||
_: 1,
|
||||
s: 'designs/tags',
|
||||
t: t('design:tags'),
|
||||
o: 'aaa',
|
||||
},
|
||||
},
|
||||
patterns: {
|
||||
m: 1,
|
||||
s: 'patterns',
|
||||
t: t('sections:patterns'),
|
||||
},
|
||||
sets: {
|
||||
m: 1,
|
||||
s: 'sets',
|
||||
t: t('sections:sets'),
|
||||
},
|
||||
community: {
|
||||
m: 1,
|
||||
s: 'community',
|
||||
t: t('sections:community'),
|
||||
},
|
||||
account: {
|
||||
m: 1,
|
||||
s: 'account',
|
||||
t: t('sections:account'),
|
||||
n: 1,
|
||||
},
|
||||
// Top-level pages that are not in the sections menu
|
||||
apikeys: {
|
||||
_: 1,
|
||||
s: 'apikeys',
|
||||
h: 1,
|
||||
t: t('apikeys'),
|
||||
},
|
||||
curate: {
|
||||
s: 'curate',
|
||||
h: 1,
|
||||
t: t('curate'),
|
||||
sets: {
|
||||
t: t('curateSets'),
|
||||
s: 'curate/sets',
|
||||
},
|
||||
},
|
||||
new: {
|
||||
m: 1,
|
||||
s: 'new',
|
||||
h: 1,
|
||||
t: t('sections:new'),
|
||||
pattern: {
|
||||
t: t('patternNew'),
|
||||
s: 'new/pattern',
|
||||
o: 10,
|
||||
},
|
||||
set: {
|
||||
t: t('newSet'),
|
||||
s: 'new/set',
|
||||
0: 20,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
s: 'profile',
|
||||
h: 1,
|
||||
t: t('yourProfile'),
|
||||
},
|
||||
translation: {
|
||||
s: 'translation',
|
||||
h: 1,
|
||||
t: t('translation'),
|
||||
join: {
|
||||
t: t('translation:joinATranslationTeam'),
|
||||
s: 'translation',
|
||||
},
|
||||
'suggest-language': {
|
||||
t: t('translation:suggestLanguage'),
|
||||
s: 'translation',
|
||||
},
|
||||
},
|
||||
sitemap: {
|
||||
s: 'sitemap',
|
||||
h: 1,
|
||||
t: t('sitemap'),
|
||||
},
|
||||
|
||||
// Not translated, this is a developer page
|
||||
typography: {
|
||||
s: 'typography',
|
||||
h: 1,
|
||||
t: 'Typography',
|
||||
},
|
||||
}
|
||||
for (const section in conf.account.fields) {
|
||||
for (const [field, controlScore] of Object.entries(conf.account.fields[section])) {
|
||||
if (Number(control) >= controlScore)
|
||||
pages.account[field] = {
|
||||
s: `account/${field}`,
|
||||
t: t(`account:${field}`),
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Number(control) >= conf.account.fields.developer.apikeys)
|
||||
pages.new.apikey = {
|
||||
s: 'new/apikey',
|
||||
t: t('newApikey'),
|
||||
o: 30,
|
||||
}
|
||||
pages.account.reload = {
|
||||
s: `account/reload`,
|
||||
t: t(`account:reload`),
|
||||
}
|
||||
for (const design in designs) {
|
||||
// pages.designs[design] = {
|
||||
// t: t(`designs:${design}.t`),
|
||||
// s: `designs/${design}`,
|
||||
// }
|
||||
pages.new.pattern[design] = {
|
||||
s: `new/${design}`,
|
||||
t: t(`account:generateANewThing`, { thing: t(`designs:${design}.t`) }),
|
||||
}
|
||||
}
|
||||
for (const tag of tags) {
|
||||
pages.designs.tags[tag] = {
|
||||
s: `designs/tags/${tag}`,
|
||||
t: t(`tags:${tag}`),
|
||||
}
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
export const useNavigation = (param = {}, extra = []) => {
|
||||
const { ignoreControl = false } = param
|
||||
// Passing in the locale is not very DRY so let's just grab it from the router
|
||||
const { locale } = useRouter()
|
||||
// We need translation
|
||||
const { t } = useTranslation(ns)
|
||||
// We need the account if we want to take control into account
|
||||
const { account } = useAccount()
|
||||
|
||||
const control = ignoreControl ? undefined : account.control
|
||||
|
||||
const value = useMemo(() => {
|
||||
const siteNav = {
|
||||
...pbn[locale],
|
||||
...sitePages(t, control),
|
||||
}
|
||||
for (const [_path, _data] of extra) {
|
||||
objUpdate(siteNav, _path, _data)
|
||||
}
|
||||
|
||||
// Apply some tweaks
|
||||
siteNav.blog.m = 1
|
||||
siteNav.blog.n = 1
|
||||
siteNav.showcase.m = 1
|
||||
siteNav.showcase.n = 1
|
||||
siteNav.docs.m = 1
|
||||
siteNav.newsletter._ = true
|
||||
|
||||
// Set order on main sections
|
||||
siteNav.designs.o = 10
|
||||
siteNav.docs.o = 20
|
||||
siteNav.blog.o = 30
|
||||
siteNav.showcase.o = 40
|
||||
siteNav.community.o = 50
|
||||
siteNav.patterns.o = 60
|
||||
siteNav.sets.o = 70
|
||||
siteNav.account.o = 80
|
||||
siteNav.new.o = 90
|
||||
|
||||
return {
|
||||
siteNav, // Site navigation
|
||||
slugLut: orderedSlugLut(siteNav), // Slug lookup table
|
||||
}
|
||||
}, [locale, extra, control])
|
||||
|
||||
return value
|
||||
}
|
|
@ -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": {},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { order } from 'site/prebuild/blog-paths.mjs'
|
||||
import { pages as posts } from 'site/prebuild/blog.mjs'
|
||||
import { getPostSlugPaths } from 'site/components/mdx/posts/utils.mjs'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { useDynamicMdx } from 'shared/hooks/use-dynamic-mdx.mjs'
|
||||
|
@ -68,7 +68,7 @@ export async function getStaticProps({ params, locale }) {
|
|||
|
||||
export const getStaticPaths = async () => {
|
||||
return {
|
||||
paths: getPostSlugPaths(order),
|
||||
paths: getPostSlugPaths(posts),
|
||||
fallback: 'blocking',
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Dependencies
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import { postInfo, order } from 'site/prebuild/blog-paths.mjs'
|
||||
import { pages as posts } from 'site/prebuild/blog.mjs'
|
||||
import { meta } from 'site/prebuild/blog-meta.mjs'
|
||||
import { getPostIndexPaths, getPostIndexProps } from 'site/components/mdx/posts/utils.mjs'
|
||||
// Hooks
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
@ -91,7 +92,7 @@ const BlogIndexPage = ({ posts, page, current, total }) => {
|
|||
export default BlogIndexPage
|
||||
|
||||
export async function getStaticProps({ locale, params }) {
|
||||
const props = getPostIndexProps(locale, params, order, postInfo)
|
||||
const props = getPostIndexProps(params.page, posts[locale], meta)
|
||||
|
||||
if (props === false) return { notFound: true }
|
||||
|
||||
|
@ -110,7 +111,7 @@ export async function getStaticProps({ locale, params }) {
|
|||
|
||||
export const getStaticPaths = async () => {
|
||||
return {
|
||||
paths: getPostIndexPaths(order, 'blog'),
|
||||
paths: getPostIndexPaths(posts, 'blog'),
|
||||
fallback: 'blocking',
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -85,7 +85,7 @@ export async function getStaticProps({ locale, params }) {
|
|||
* To learn more, see: https://nextjs.org/docs/basic-features/data-fetching
|
||||
*/
|
||||
export async function getStaticPaths() {
|
||||
const somePaths = mdxPaths
|
||||
const somePaths = Object.keys(pages)
|
||||
.filter((path) => path.split('/').length < 5)
|
||||
.filter((path) => path !== 'docs')
|
||||
|
||||
|
@ -96,6 +96,7 @@ export async function getStaticPaths() {
|
|||
...somePaths.map((key) => `/de/${key}`),
|
||||
...somePaths.map((key) => `/fr/${key}`),
|
||||
...somePaths.map((key) => `/nl/${key}`),
|
||||
...somePaths.map((key) => `/uk/${key}`),
|
||||
],
|
||||
fallback: 'blocking',
|
||||
}
|
||||
|
|
|
@ -5,9 +5,8 @@ import { useTranslation } from 'next-i18next'
|
|||
// Components
|
||||
import { PageWrapper, ns as pageNs } from 'shared/components/wrappers/page.mjs'
|
||||
import { ReadMore } from 'shared/components/mdx/read-more.mjs'
|
||||
import { ns as navNs } from 'site/hooks/use-navigation.mjs'
|
||||
|
||||
const ns = [...pageNs, navNs, 'common']
|
||||
const ns = [...pageNs, 'common']
|
||||
|
||||
const SitemapPage = ({ page }) => {
|
||||
const { t } = useTranslation(ns)
|
||||
|
|
90
sites/org/prebuild.mjs
Normal file
90
sites/org/prebuild.mjs
Normal file
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -20,4 +20,8 @@ export const siteConfig = {
|
|||
languagesWip: [],
|
||||
site: 'FreeSewing.org',
|
||||
tld: 'org',
|
||||
posts: {
|
||||
preGenerate: 6,
|
||||
perPage: 12,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* The default full-page FreeSewing layout
|
||||
*/
|
||||
export const BaseLayout = ({ children = [] }) => (
|
||||
<div className="flex flex-row items-start mt-8 w-full justify-between 2xl:px-36 xl:px-12 px-4">
|
||||
<div className="flex flex-row items-start mt-8 w-full justify-between 2xl:px-36 xl:px-12 px-4 gap-0 lg:gap-4 xl:gap-8 3xl: gap-12">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -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 }) => {
|
|||
<h5 className="flex flex-row items-center justify-between w-full">
|
||||
<span>{t(`designs:${name}.t`)}</span>
|
||||
<span className="flex flex-col items-end">
|
||||
<span className="text-xs font-medium opacity-70">{t('design:difficulty')}</span>
|
||||
<span className="text-xs font-medium opacity-70">{t('tags:difficulty')}</span>
|
||||
<Difficulty score={designs[name].difficulty} />
|
||||
</span>
|
||||
</h5>
|
||||
|
|
|
@ -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 (
|
||||
<footer className="bg-neutral">
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useContext } from 'react'
|
||||
import { NavigationContext } from 'shared/context/navigation-context.mjs'
|
||||
import get from 'lodash.get'
|
||||
import { useNavigation } from 'site/hooks/use-navigation.mjs'
|
||||
import Link from 'next/link'
|
||||
|
||||
const getPage = {
|
||||
|
@ -13,7 +14,7 @@ const getPage = {
|
|||
}
|
||||
|
||||
export const DocsTitle = ({ slug, className = '', site = 'org' }) => {
|
||||
const { siteNav } = useNavigation()
|
||||
const { siteNav } = useContext(NavigationContext)
|
||||
const page = getPage[site](slug, siteNav)
|
||||
|
||||
return page ? <span className={className}>{page.t}</span> : null
|
||||
|
|
|
@ -93,7 +93,6 @@ export const MdxMetaData = ({ frontmatter, locale, slug }) => {
|
|||
className="btn btn-success flex flex-row justify-between items-center w-full px-4 bg-gradient-to-r from-primary to-accent mb-4 hover:from-accent hover:to-accent"
|
||||
>
|
||||
<EditIcon />
|
||||
<span>Found a mistake?</span>
|
||||
<span>{t('editThisPage')}</span>
|
||||
</a>
|
||||
<div
|
||||
|
|
|
@ -2,7 +2,6 @@ import get from 'lodash.get'
|
|||
import Link from 'next/link'
|
||||
import { useContext } from 'react'
|
||||
import { NavigationContext } from 'shared/context/navigation-context.mjs'
|
||||
import { useNavigation } from 'site/hooks/use-navigation.mjs'
|
||||
import { BulletIcon, RightIcon } from 'shared/components/icons.mjs'
|
||||
import { pageHasChildren } from 'shared/utils.mjs'
|
||||
import orderBy from 'lodash.orderby'
|
||||
|
@ -70,15 +69,8 @@ const RenderTree = ({ tree, recurse, depth = 1, level = 0 }) => {
|
|||
)
|
||||
}
|
||||
|
||||
export const ReadMore = ({
|
||||
recurse = 0,
|
||||
root = false,
|
||||
site = 'org',
|
||||
depth = 99,
|
||||
ignoreControl,
|
||||
}) => {
|
||||
const { slug } = useContext(NavigationContext)
|
||||
const { siteNav } = useNavigation({ ignoreControl })
|
||||
export const ReadMore = ({ recurse = 0, root = false, site = 'org', depth = 99 }) => {
|
||||
const { siteNav, slug } = useContext(NavigationContext)
|
||||
|
||||
// Deal with recurse not being a number
|
||||
if (recurse && recurse !== true) {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { useContext } from 'react'
|
||||
import { NavigationContext } from 'shared/context/navigation-context.mjs'
|
||||
import Link from 'next/link'
|
||||
import { pageHasChildren, isSlugPart } from 'shared/utils.mjs'
|
||||
import get from 'lodash.get'
|
||||
|
@ -130,10 +132,12 @@ const MainLink = ({
|
|||
/*
|
||||
* A React component to render breadcrumbs to the current page
|
||||
*
|
||||
* @param slug {string} - The slug of the current page
|
||||
* @param siteNav {object} - The site navigation object as returned by the useNavigation hook
|
||||
* @param lead {string} - A lead to display before the cumbs (eg: You are here)
|
||||
*/
|
||||
export const Breadcrumbs = ({ slug = false, lead = false, siteNav }) => {
|
||||
export const Breadcrumbs = ({ lead = false }) => {
|
||||
// Grab siteNav and slug from the navigation context
|
||||
const { siteNav, slug } = useContext(NavigationContext)
|
||||
|
||||
const { t } = useTranslation(['common'])
|
||||
|
||||
if (slug === false) {
|
||||
|
@ -193,33 +197,34 @@ export const Breadcrumbs = ({ slug = false, lead = false, siteNav }) => {
|
|||
/*
|
||||
* A React component to render sidebar navigation based on the siteNav object and current slug
|
||||
*
|
||||
* The main sections are determined in the use-navigation hook.
|
||||
* The main sections are determined in the navigation prebuild code.
|
||||
* We always display the navigation as:
|
||||
* - Always show all top-level entries
|
||||
* - Always show all direct children of all top-level entries (this allows for better discoverability)
|
||||
* - If we're deeper down, only expand the active page
|
||||
*
|
||||
* @param slug {string} - The slug of the current page
|
||||
* @param siteNav {object} - The siteNav object from the useNavigation hook
|
||||
*/
|
||||
export const NavLinks = ({ slug, siteNav }) => (
|
||||
<ul className="w-full list mb-8 mt-3">
|
||||
{onlyValidChildren(siteNav).map((page, i) => (
|
||||
<li key={i} className="w-full">
|
||||
<MainLink s={page.s} t={page.t} slug={slug} />
|
||||
{pageHasChildren(page) && !page.n && <Section {...{ tree: page, slug }} />}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
export const NavLinks = () => {
|
||||
// Grab siteNav and slug from the navigation context
|
||||
const { siteNav, slug } = useContext(NavigationContext)
|
||||
|
||||
return (
|
||||
<ul className="w-full list mb-8 mt-3">
|
||||
{onlyValidChildren(siteNav).map((page, i) => (
|
||||
<li key={i} className="w-full">
|
||||
<MainLink s={page.s} t={page.t} slug={slug} />
|
||||
{pageHasChildren(page) && !page.n && <Section {...{ tree: page, slug }} />}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* A React component to render sidebar navigation for the main sections
|
||||
*
|
||||
* @param siteNav {object} - The siteNav object from the useNavigation hook
|
||||
* @param slug {string} - The slug of the current page
|
||||
*/
|
||||
export const MainSections = ({ siteNav, slug }) => {
|
||||
export const MainSections = () => {
|
||||
// Grab siteNav and slug from the navigation context
|
||||
const { siteNav, slug } = useContext(NavigationContext)
|
||||
const output = []
|
||||
for (const page of onlyMainSections(siteNav)) {
|
||||
const act = isSlugPart(page.s, slug)
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
// Dependencies
|
||||
import get from 'lodash.get'
|
||||
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 { LeftIcon, RightIcon } from 'shared/components/icons.mjs'
|
||||
|
@ -39,9 +40,9 @@ const getItemWithCaveat = (index, slugLut, siteNav, shouldHide) => {
|
|||
return get(siteNav, slugLut[index].split('/'))
|
||||
}
|
||||
|
||||
export const PrevNext = ({ slug, noPrev = false, noNext = false }) => {
|
||||
// Grab site navigation and slug lookup table from the useNavigatin hook
|
||||
const { siteNav, slugLut } = useNavigation()
|
||||
export const PrevNext = ({ noPrev = false, noNext = false }) => {
|
||||
// Grab siteNav and slugLut from the navigation context
|
||||
const { siteNav, slugLut, slug } = useContext(NavigationContext)
|
||||
|
||||
// Lookup the current slug in the LUT
|
||||
const index = slugLut.indexOf(slug)
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
//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']
|
||||
|
||||
export const Set = ({ name }) => {
|
||||
export const Set = () => {
|
||||
//const { t } = useTranslation(ns)
|
||||
|
||||
return <p>fixme</p>
|
||||
|
@ -21,7 +21,7 @@ export const Set = ({ name }) => {
|
|||
<h5 className="flex flex-row items-center justify-between w-full">
|
||||
<span>{t(`designs:${name}.t`)}</span>
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-xs font-medium opacity-70">{t('design:difficulty')}</span>
|
||||
<span className="text-xs font-medium opacity-70">{t('tags:difficulty')}</span>
|
||||
<Difficulty score={designs[name].difficulty} />
|
||||
</div>
|
||||
</h5>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// Dependencies
|
||||
import React, { useState, useEffect, useContext } from 'react'
|
||||
//import { useHotkeys } from 'react-hotkeys-hook'
|
||||
// Hooks
|
||||
import { useTheme } from 'shared/hooks/use-theme.mjs'
|
||||
// Components
|
||||
|
@ -10,7 +9,6 @@ import { LayoutWrapper, ns as layoutNs } from 'shared/components/wrappers/layout
|
|||
import { DefaultLayout, ns as defaultLayoutNs } from 'site/components/layouts/default.mjs'
|
||||
import { Feeds } from 'site/components/feeds.mjs'
|
||||
import { ModalContext } from 'shared/context/modal-context.mjs'
|
||||
import { NavigationContext } from 'shared/context/navigation-context.mjs'
|
||||
|
||||
export const ns = [...new Set([...layoutNs, ...defaultLayoutNs])]
|
||||
|
||||
|
@ -19,14 +17,7 @@ export const PageWrapper = (props) => {
|
|||
/*
|
||||
* Deconstruct props
|
||||
*/
|
||||
const {
|
||||
layout = DefaultLayout,
|
||||
footer = true,
|
||||
header = false,
|
||||
children = [],
|
||||
path = [],
|
||||
locale = 'en',
|
||||
} = props
|
||||
const { layout = DefaultLayout, footer = true, header = false, children = [], path = [] } = props
|
||||
// Title is typically set in props.t but check props.title too
|
||||
const pageTitle = props.t ? props.t : props.title ? props.title : null
|
||||
|
||||
|
@ -40,7 +31,6 @@ export const PageWrapper = (props) => {
|
|||
* Contexts
|
||||
*/
|
||||
const { modalContent } = useContext(ModalContext)
|
||||
const { setNavigation } = useContext(NavigationContext)
|
||||
|
||||
/*
|
||||
* This forces a re-render upon initial bootstrap of the app
|
||||
|
@ -50,29 +40,6 @@ export const PageWrapper = (props) => {
|
|||
const [currentTheme, setCurrentTheme] = useState()
|
||||
useEffect(() => setCurrentTheme(theme), [currentTheme, theme])
|
||||
|
||||
/*
|
||||
* Update navigation context with title and path
|
||||
*/
|
||||
useEffect(() => {
|
||||
setNavigation({
|
||||
title: pageTitle,
|
||||
locale,
|
||||
path,
|
||||
})
|
||||
}, [path, pageTitle, locale, setNavigation])
|
||||
|
||||
/*
|
||||
* Hotkeys (keyboard actions)
|
||||
*/
|
||||
// Trigger search with /
|
||||
//useHotkeys('/', (evt) => {
|
||||
// evt.preventDefault()
|
||||
// setSearch(true)
|
||||
//})
|
||||
|
||||
// Search state
|
||||
//const [search, setSearch] = useState(false)
|
||||
|
||||
// Helper object to pass props down (keeps things DRY)
|
||||
const childProps = { footer, header, pageTitle, slug }
|
||||
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import allDesigns from 'config/software/designs.json'
|
||||
/*
|
||||
* Do not use the 'config' webpack alias here because
|
||||
* this is used in the prebuild step which is pure NodeJS
|
||||
*/
|
||||
import allDesigns from '../../../config/software/designs.json' assert { type: 'json' }
|
||||
|
||||
/*
|
||||
* Filter out utility patterns by checking whether they have any tags
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import { social } from 'config/social.mjs'
|
||||
/*
|
||||
* Do not use the 'config' webpack alias here because
|
||||
* this is used in the prebuild step which is pure NodeJS
|
||||
*/
|
||||
import { social } from '../../../config//social.mjs'
|
||||
|
||||
export const freeSewingConfig = {
|
||||
monorepo: 'https://github.com/freesewing/freesewing',
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from 'react'
|
||||
import { createContext, useState } from 'react'
|
||||
|
||||
export const ModalContext = React.createContext(null)
|
||||
export const ModalContext = createContext(null)
|
||||
|
||||
export const ModalContextProvider = ({ children }) => {
|
||||
function clearModal() {
|
||||
|
|
|
@ -1,80 +1,95 @@
|
|||
import orderBy from 'lodash.orderby'
|
||||
import get from 'lodash.get'
|
||||
import React, { useState } from 'react'
|
||||
import { useNavigation } from 'site/hooks/use-navigation.mjs'
|
||||
import { createContext, useState } from 'react'
|
||||
import { objUpdate } from 'shared/utils.mjs'
|
||||
import { siteNav as defaultSiteNav } from 'site/prebuild/sitenav.mjs'
|
||||
import { slugLut as defaultSlugLut } from 'site/prebuild/slugLut.mjs'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const defaultNavigationContext = {
|
||||
path: [],
|
||||
title: 'FIXME: No title (default)',
|
||||
locale: 'en',
|
||||
crumbs: [],
|
||||
}
|
||||
|
||||
export const NavigationContext = React.createContext(defaultNavigationContext)
|
||||
|
||||
const createCrumbs = (path, nav) =>
|
||||
path.map((crumb, i) => {
|
||||
const slice = path.slice(0, i + 1)
|
||||
const entry = get(nav, slice, { t: 'no-actual-title', s: slice.join('/') })
|
||||
const val = { t: entry.t, s: entry.s }
|
||||
if (entry.o) val.o = entry.o
|
||||
|
||||
return val
|
||||
})
|
||||
|
||||
const createSections = (nav) => {
|
||||
const sections = {}
|
||||
for (const slug of Object.keys(nav)) {
|
||||
const entry = nav[slug]
|
||||
const val = { t: entry.t, s: entry.s }
|
||||
if (entry.o) val.o = entry.o
|
||||
if (!entry.h) sections[slug] = val
|
||||
}
|
||||
|
||||
return orderBy(sections, ['o', 't'])
|
||||
}
|
||||
|
||||
const buildNavState = (value, siteNav, extra = []) => {
|
||||
for (const [path, data] of extra) {
|
||||
siteNav = objUpdate(siteNav, path, data)
|
||||
}
|
||||
const obj = {
|
||||
siteNav,
|
||||
crumbs: createCrumbs(value.path, siteNav),
|
||||
sections: createSections(siteNav),
|
||||
slug: value.path.join('/'),
|
||||
}
|
||||
obj.title = obj.crumbs.length > 0 ? obj.crumbs.slice(-1)[0].t : ''
|
||||
|
||||
return obj
|
||||
}
|
||||
/*
|
||||
* The context, which uses the default pre-build data
|
||||
*/
|
||||
export const NavigationContext = createContext({
|
||||
siteNav: defaultSiteNav,
|
||||
slugLut: defaultSlugLut,
|
||||
})
|
||||
|
||||
/*
|
||||
* The context provider which will pass the value down
|
||||
*/
|
||||
export const NavigationContextProvider = ({ children }) => {
|
||||
function setNavigation(newValues) {
|
||||
setValue({
|
||||
...value,
|
||||
...newValues,
|
||||
setNavigation,
|
||||
addPages,
|
||||
})
|
||||
/*
|
||||
* Get the locale and slug from the Next's router object
|
||||
*/
|
||||
const { locale, asPath } = useRouter()
|
||||
const slug = asPath.slice(1) // Strip the leading slash
|
||||
|
||||
/*
|
||||
* Helper method to hot-update the siteNav object
|
||||
* This object is created in the prebuild step and holds all site pages.
|
||||
* However, sometimes we want to update it with user-generated pages such
|
||||
* as individual patterns or measurments sets
|
||||
*
|
||||
* This uses objUpdate() from utils, which is just a wrapper around lodash.set
|
||||
* that has additional functionality to unset/delete values.
|
||||
*
|
||||
* @param path {string} - Path to the value to update. See lodash.set for details.
|
||||
* @param value {value} to set
|
||||
*/
|
||||
function updateSiteNav(path, value) {
|
||||
setSiteNav(objUpdate(siteNav[locale], path, value))
|
||||
}
|
||||
|
||||
const [value, setValue] = useState({
|
||||
...defaultNavigationContext,
|
||||
setNavigation,
|
||||
})
|
||||
const [extraPages, setExtraPages] = useState([])
|
||||
|
||||
const { siteNav } = useNavigation({ path: value.path, locale: value.locale }, extraPages)
|
||||
const navState = buildNavState(value, siteNav)
|
||||
|
||||
const addPages = (extra) => {
|
||||
setExtraPages([...extraPages, ...extra])
|
||||
/*
|
||||
* Helper method to update the slugLut
|
||||
* This is a list of slugs of all the pages in the order one would expect them.
|
||||
* This makes it easy to put a 'next page' or 'previous page' link as we just grab
|
||||
* the next/previous entry on the list and lookup its title in the siteNav objectt.
|
||||
*
|
||||
* Currently not implemented: How do we update this for user-generated content?
|
||||
* Perhaps something like: updateSlugLut('after', slug)
|
||||
* We'll do this later. Not even certain it's needed as we may just not place
|
||||
* previous/next links on user-generated content.
|
||||
*/
|
||||
function updateSlugLut() {
|
||||
// FIXME: Is this even needed?
|
||||
console.log('updateSlugLut is not implemented (yet)')
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method for when we want to update both the siteNav and slugLut object in one call
|
||||
*/
|
||||
function update(obj) {
|
||||
if (obj.siteNav) updateSiteNav(...obj.siteNav)
|
||||
if (obj.slugLut) updateSlugLut(...obj.slugLut)
|
||||
}
|
||||
|
||||
/*
|
||||
* Local state
|
||||
* Remember that only a state of prop change will trigger a re-render.
|
||||
* So if we want changes to the context to propogate throughout the components
|
||||
* using the context, we need to make the context value itself a state value
|
||||
* so that updating it will trigger a re-render and the propagated value will update.
|
||||
*/
|
||||
const [siteNav, setSiteNav] = useState(defaultSiteNav)
|
||||
const [slugLut] = useState(defaultSlugLut)
|
||||
|
||||
/*
|
||||
* Pass everything down as the value object, including the methods to update
|
||||
* the state (which will in turn update the context value)
|
||||
*
|
||||
* Note that we're only passing down the siteNav object for the current locale
|
||||
*/
|
||||
return (
|
||||
<NavigationContext.Provider value={{ ...value, ...navState }}>
|
||||
<NavigationContext.Provider
|
||||
value={{
|
||||
siteNav: siteNav[locale],
|
||||
slugLut,
|
||||
slug,
|
||||
locale,
|
||||
updateSiteNav,
|
||||
updateSlugLut,
|
||||
update,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NavigationContext.Provider>
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { pageHasChildren } from 'shared/utils.mjs'
|
||||
import { pageHasChildren } from '../utils.mjs'
|
||||
import orderBy from 'lodash.orderby'
|
||||
|
||||
/*
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
difficulty: Difficulty
|
||||
tags: Design tags
|
||||
accessories: Accessories
|
||||
bags: Bags
|
||||
blocks: Blocks
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
]
|
||||
|
|
|
@ -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 } },
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,177 +0,0 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import rdir from 'recursive-readdir'
|
||||
import { unified } from 'unified'
|
||||
import remarkParser from 'remark-parse'
|
||||
import remarkCompiler from 'remark-stringify'
|
||||
import remarkFrontmatter from 'remark-frontmatter'
|
||||
import remarkFrontmatterExtractor from 'remark-extract-frontmatter'
|
||||
import { readSync } from 'to-vfile'
|
||||
import yaml from 'js-yaml'
|
||||
import { mdIntro } from './md-intro.mjs'
|
||||
|
||||
// Some arbitrary future time
|
||||
const future = new Date('10-12-2026').getTime()
|
||||
|
||||
export const header = `/*
|
||||
*
|
||||
* This page was auto-generated by the prebuild script
|
||||
* Any changes you make to it will be lost on the next (pre)build.
|
||||
*/
|
||||
`
|
||||
|
||||
/*
|
||||
* There's an issue in crowdin where it changes the frontmatter marker:
|
||||
* ---
|
||||
* into this:
|
||||
* - - -
|
||||
* which breaks stuff. So this method takes the input and replaces all
|
||||
* - - - with ---
|
||||
*/
|
||||
export const fixCrowdinBugs = (md) => {
|
||||
md.value = md.value.split('- - -\n').join('---\n')
|
||||
return md
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to get the title and meta data from an MDX file
|
||||
*
|
||||
* Parameters:
|
||||
*
|
||||
* - file: the full path to the file
|
||||
*/
|
||||
export const mdxMetaInfo = async (file) => {
|
||||
let result
|
||||
try {
|
||||
result = await unified()
|
||||
.use(remarkParser)
|
||||
.use(remarkCompiler)
|
||||
.use(remarkFrontmatter)
|
||||
.use(remarkFrontmatterExtractor, { yaml: yaml.load })
|
||||
.process(fixCrowdinBugs(readSync(file, { encoding: 'utf-8' })))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to get a list of MDX files in a folder.
|
||||
* Will traverse recursively to get all files from a given root folder.
|
||||
*
|
||||
* Parameters:
|
||||
*
|
||||
* - folder: the root folder to look in
|
||||
* - lang: the language files to looks for
|
||||
*
|
||||
* Exported because it's also used by the Algolia index script
|
||||
*/
|
||||
export const getMdxFileList = async (folder, lang) => {
|
||||
let allFiles
|
||||
try {
|
||||
allFiles = await rdir(folder)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter out all that's not a language-specific markdown file
|
||||
// and avoid including the 'ui' files
|
||||
const files = []
|
||||
for (const file of allFiles) {
|
||||
if (file.slice(-5) === `${lang}.md`) files.push(file)
|
||||
}
|
||||
|
||||
return files.sort()
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to get the website slug (path) from the file path
|
||||
*/
|
||||
export const fileToSlug = (file, site, lang) =>
|
||||
file.slice(-6) === `/${lang}.md` ? file.split(`/markdown/${site}/`).pop().slice(0, -6) : false
|
||||
|
||||
export const loadMdxForPrebuild = async (site, folder, locales) => {
|
||||
const pages = {}
|
||||
// Loop over languages
|
||||
for (const lang of locales) {
|
||||
pages[lang] = {}
|
||||
// Get list of filenames
|
||||
const list = await getMdxFileList(folder, lang)
|
||||
|
||||
// Loop over files
|
||||
for (const file of list) {
|
||||
const slug = fileToSlug(file, site, lang)
|
||||
const meta = await mdxMetaInfo(file)
|
||||
// minify the metadat
|
||||
if (meta.data?.title) {
|
||||
const minMeta = { t: meta.data.title }
|
||||
|
||||
if (meta.data.order) minMeta.o = `${meta.data.order}${meta.data.title}`
|
||||
else if (meta.data.date) {
|
||||
minMeta.d = meta.data.date
|
||||
minMeta.o = (future - new Date(meta.data.date).getTime()) / 100000
|
||||
}
|
||||
|
||||
if (meta.data.image) minMeta.i = meta.data.image
|
||||
if (meta.data.author) minMeta.a = meta.data.author
|
||||
if (meta.data.maker) minMeta.a = meta.data.maker
|
||||
pages[lang][slug] = minMeta
|
||||
} else {
|
||||
if (pages.en[slug]) {
|
||||
console.log(`⚠️l Falling back to EN metadata for ${lang} ${slug}`)
|
||||
pages[lang][slug] = pages.en[slug]
|
||||
} else {
|
||||
console.log(`❌ [${lang}] Failed to extract meta info from: ${slug}`)
|
||||
if (meta.messages.length > 0) console.log(meta.messages)
|
||||
}
|
||||
}
|
||||
const intros = {}
|
||||
intros[lang] = await mdIntro(lang, site, slug)
|
||||
//if (process.env.GENERATE_OG_IMAGES) {
|
||||
// // Create og image
|
||||
// await generateOgImage({ lang, site, slug, title: meta.data.title, intro: intros[lang] })
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
return pages
|
||||
}
|
||||
/*
|
||||
* Main method that does what needs doing
|
||||
*/
|
||||
export const prebuildDocs = async (site) => {
|
||||
// Say hi
|
||||
console.log()
|
||||
console.log(`Compiling list of docs pages for freesewing.${site}`)
|
||||
|
||||
// Languages
|
||||
const locales = site === 'dev' ? ['en'] : ['en', 'fr', 'es', 'nl', 'de', 'uk']
|
||||
|
||||
// Setup MDX root path
|
||||
const root = ['..', '..', 'markdown', site]
|
||||
if (site === 'org') root.push('docs')
|
||||
const mdxRoot = path.resolve(...root)
|
||||
|
||||
const pages = await loadMdxForPrebuild(site, mdxRoot, locales)
|
||||
|
||||
// Write files with MDX paths
|
||||
let allPaths = ``
|
||||
for (const lang of locales) {
|
||||
fs.writeFileSync(
|
||||
path.resolve('..', site, 'prebuild', `mdx-paths.${lang}.mjs`),
|
||||
`${header}export const mdxPaths = ${JSON.stringify(Object.keys(pages[lang]))}`
|
||||
)
|
||||
allPaths += `import { mdxPaths as ${lang} } from './mdx-paths.${lang}.mjs'` + '\n'
|
||||
}
|
||||
// Write umbrella file
|
||||
fs.writeFileSync(
|
||||
path.resolve('..', site, 'prebuild', `mdx-paths.mjs`),
|
||||
`${allPaths}${header}
|
||||
|
||||
export const mdxPaths = { ${locales.join(',')} }`
|
||||
)
|
||||
|
||||
return pages
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -1,19 +1,27 @@
|
|||
import execa from 'execa'
|
||||
import { exec } from 'node:child_process'
|
||||
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 = '____'
|
||||
|
||||
const parseLog = (line) => line.split(divider).map((item) => item.trim())
|
||||
|
||||
/*
|
||||
* Helper method to get the website slug (path) from the file path
|
||||
*/
|
||||
const fileToSlug = (file, site, lang) =>
|
||||
file.slice(-6) === `/${lang}.md` ? file.split(`/markdown/${site}/`).pop().slice(0, -6) : false
|
||||
|
||||
/*
|
||||
* Extracts git authors and last modification date from git log.
|
||||
* Strictly speaking, it's the last commit date, but you get the idea.
|
||||
*/
|
||||
export const getGitMetadata = async (file, site) => {
|
||||
const slug = fileToSlug(file, site, 'en')
|
||||
if (!slug) console.log({ file, slug })
|
||||
const log = await execa.command(
|
||||
`git log --pretty="format:%cs${divider}%aN${divider}%aE" ${file}`,
|
||||
{ shell: true }
|
||||
|
@ -26,12 +34,10 @@ export const getGitMetadata = async (file, site) => {
|
|||
if (!lastUpdated) lastUpdated = date.split('-').join('')
|
||||
let key = false
|
||||
if (typeof authorInfo[author] !== 'undefined') key = author
|
||||
else if (typeof authorInfo[email] !== 'undefined') key = author
|
||||
else {
|
||||
if (typeof gitToAuthor[author] !== 'undefined') {
|
||||
key = gitToAuthor[author]
|
||||
} else if (typeof gitToAuthor[email] !== 'undefined') {
|
||||
key = gitToAuthor[email]
|
||||
}
|
||||
if (typeof gitToAuthor[author] !== 'undefined') key = gitToAuthor[author]
|
||||
else if (typeof gitToAuthor[email] !== 'undefined') key = gitToAuthor[email]
|
||||
}
|
||||
if (!key) {
|
||||
if (typeof email === 'undefined') {
|
||||
|
@ -39,8 +45,9 @@ export const getGitMetadata = async (file, site) => {
|
|||
authors.add('unknown')
|
||||
} else {
|
||||
// There is a git history, but the author is not known
|
||||
console.log({ email, author, slug })
|
||||
throw `Git author email ${email} is unknown in the git-to-author table`
|
||||
console.log('Missing git author info for:', { email, author, slug })
|
||||
// Don't throw, it's annotying
|
||||
//throw `Git author email ${email} is unknown in the git-to-author table`
|
||||
}
|
||||
} else authors.add(key)
|
||||
}
|
||||
|
@ -53,49 +60,101 @@ export const getGitMetadata = async (file, site) => {
|
|||
}
|
||||
|
||||
/*
|
||||
* Main method that does what needs doing
|
||||
* Writes data to the prebuild files
|
||||
*/
|
||||
export const prebuildGitData = async (site) => {
|
||||
// Say hi
|
||||
console.log()
|
||||
console.log(`Prebuilding git author data for freesewing.${site}`)
|
||||
|
||||
// Setup MDX root path
|
||||
const root = ['..', '..', 'markdown', site]
|
||||
if (site === 'org') root.push('docs')
|
||||
const mdxRoot = path.resolve(...root)
|
||||
|
||||
const pages = {}
|
||||
|
||||
// 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 writeData = async (store) => {
|
||||
// Write page to disk
|
||||
const dir = path.resolve('..', site, 'prebuild')
|
||||
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(pages)}`
|
||||
`export const docUpdates = ${JSON.stringify(store.git.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]++
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(dir, `doc-stats.mjs`),
|
||||
`export const docStats = ${JSON.stringify(stats, null, 2)}`
|
||||
`export const docStats = ${JSON.stringify(store.git.stats)}`
|
||||
)
|
||||
|
||||
return pages
|
||||
}
|
||||
|
||||
/*
|
||||
* Helper method to load all MDX files from a folder
|
||||
*/
|
||||
const getMdxFileList = async (cwd) => {
|
||||
const cmd = `find ${cwd} -type f -name "en.md"`
|
||||
const find = exec(cmd, { cwd }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`exec error: ${error} - ${stderr}`)
|
||||
return
|
||||
}
|
||||
|
||||
return stdout
|
||||
})
|
||||
|
||||
/*
|
||||
* Stdout is buffered, so we need to gather all of it
|
||||
*/
|
||||
let stdout = ''
|
||||
for await (const data of find.stdout) stdout += data
|
||||
|
||||
/*
|
||||
* Rerturn all matches as a sorted array
|
||||
*/
|
||||
return stdout.split('\n').sort()
|
||||
}
|
||||
|
||||
/*
|
||||
* Main method that does what needs doing
|
||||
*/
|
||||
export const prebuildGitData = async (store, mock) => {
|
||||
if (mock) {
|
||||
store.git = mockedData(store)
|
||||
return writeData(store)
|
||||
}
|
||||
|
||||
// Setup MDX root path
|
||||
const root = ['..', '..', 'markdown', store.site]
|
||||
if (store.site === 'org') root.push('docs')
|
||||
const mdxRoot = path.resolve(...root)
|
||||
|
||||
store.git = {
|
||||
pages: {},
|
||||
stats: {},
|
||||
}
|
||||
|
||||
// Get list of filenames
|
||||
const list = await getMdxFileList(mdxRoot)
|
||||
// Loop over files
|
||||
for (const file of list) {
|
||||
// This list will include '' which we don't want to get the git log for as that
|
||||
// will return the entire history
|
||||
if (file) {
|
||||
const { lastUpdated, authors, slug } = await getGitMetadata(file, store.site)
|
||||
store.git.pages[slug] = { u: lastUpdated, a: [...authors] }
|
||||
}
|
||||
}
|
||||
|
||||
// How about some stats
|
||||
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]++
|
||||
}
|
||||
}
|
||||
|
||||
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) pages[slug] = { u, a: ['mocked'] }
|
||||
|
||||
return {
|
||||
pages,
|
||||
stats: { mocked: store.navigation.sluglut.length },
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
@ -1,60 +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 { prebuildPosts } from './posts.mjs'
|
||||
import { prebuildCrowdin } from './crowdin.mjs'
|
||||
|
||||
const run = async () => {
|
||||
const now = Date.now()
|
||||
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)
|
||||
await prebuildCrowdin()
|
||||
}
|
||||
const docPages = await prebuildDocs(SITE)
|
||||
const postPages = await prebuildPosts(SITE)
|
||||
prebuildNavigation(docPages, postPages, 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('completed prebuild in ', Date.now() - now, 'ms')
|
||||
console.log()
|
||||
}
|
||||
|
||||
run()
|
284
sites/shared/prebuild/markdown.mjs
Normal file
284
sites/shared/prebuild/markdown.mjs
Normal file
|
@ -0,0 +1,284 @@
|
|||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { exec } from 'node:child_process'
|
||||
import orderBy from 'lodash.orderby'
|
||||
|
||||
/*
|
||||
* 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, lang = false) => {
|
||||
const prefix = site === 'org' ? `${folder}/` : ''
|
||||
/*
|
||||
* Figure out what directory to spawn the child process in
|
||||
*/
|
||||
const cwd = await path.resolve(process.cwd(), '..', '..', 'markdown', site, folder)
|
||||
/*
|
||||
* When going through a small number of files in a flat directory (eg. blog posts) a
|
||||
* recursive grep through all files is faster.
|
||||
* But the biggest task is combing through all the org documentation and for this
|
||||
* it's much faster to first run find to limit the number of files to open
|
||||
*/
|
||||
const cmd = `find ${cwd} -type f -name "${
|
||||
lang ? lang : '*'
|
||||
}.md" -exec grep "^${key}:" -ism 1 {} +`
|
||||
const grep = exec(cmd, { cwd }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`exec error: ${error} - ${stderr}`)
|
||||
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 === 'dev' ? '' : folder + '/'}`)
|
||||
.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, withSlug = false) => {
|
||||
const pages = {}
|
||||
for (const lang in titles) {
|
||||
pages[lang] = {}
|
||||
for (const [slug, t] of Object.entries(titles[lang])) {
|
||||
pages[lang][slug] = { t }
|
||||
if (order.en[slug]) pages[lang][slug].o = order.en[slug]
|
||||
if (withSlug) pages[lang][slug].s = 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)
|
||||
// Order is the same for all languages, so only grab EN files
|
||||
const order = await loadFolderFrontmatter('order', site, folder, false, 'en')
|
||||
|
||||
return mergeOrder(titles, order)
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads all blog posts, titles and order
|
||||
*/
|
||||
const loadBlog = async () => {
|
||||
const titles = await loadFolderFrontmatter('title', 'org', 'blog')
|
||||
// Order is the same for all languages, so only grab EN files
|
||||
const order = await loadFolderFrontmatter('date', 'org', 'blog', formatDate, 'en')
|
||||
// Author is the same for all languages, so only grab EN files
|
||||
const authors = await loadFolderFrontmatter('author', 'org', 'blog', false, 'en')
|
||||
// Image is the same for all languages, so only grab EN files
|
||||
const images = await loadFolderFrontmatter('image', 'org', 'blog', false, 'en')
|
||||
|
||||
// Merge titles and order for EN
|
||||
const merged = {}
|
||||
for (const slug in titles.en)
|
||||
merged[slug] = {
|
||||
t: titles.en[slug],
|
||||
o: order.en[slug],
|
||||
s: slug,
|
||||
a: authors.en[slug],
|
||||
i: images.en[slug],
|
||||
}
|
||||
// Order based on post data (descending)
|
||||
const ordered = orderBy(merged, 'o', 'desc')
|
||||
|
||||
// Apply same order to all languages
|
||||
const posts = {}
|
||||
const meta = {}
|
||||
|
||||
for (const lang of Object.keys(titles)) {
|
||||
posts[lang] = {}
|
||||
for (const post of ordered) {
|
||||
posts[lang][post.s] = { t: post.t }
|
||||
if (lang === 'en') meta[post.s] = { a: post.a, d: post.o, i: post.i }
|
||||
}
|
||||
}
|
||||
|
||||
return { posts, meta }
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads all showcase posts, titles and order
|
||||
*/
|
||||
const loadShowcase = async () => {
|
||||
const titles = await loadFolderFrontmatter('title', 'org', 'showcase')
|
||||
// Order is the same for all languages, so only grab EN files
|
||||
const order = await loadFolderFrontmatter('date', 'org', 'showcase', formatDate, 'en')
|
||||
// Author is the same for all languages, so only grab EN files
|
||||
const makers = await loadFolderFrontmatter('maker', 'org', 'showcase', false, 'en')
|
||||
// Image is the same for all languages, so only grab EN files
|
||||
const images = await loadFolderFrontmatter('image', 'org', 'showcase', false, 'en')
|
||||
|
||||
// Merge titles and order for EN
|
||||
const merged = {}
|
||||
for (const slug in titles.en)
|
||||
merged[slug] = {
|
||||
t: titles.en[slug],
|
||||
o: order.en[slug],
|
||||
s: slug,
|
||||
m: makers.en[slug],
|
||||
i: images.en[slug],
|
||||
}
|
||||
// Order based on post data (descending)
|
||||
const ordered = orderBy(merged, 'o', 'desc')
|
||||
|
||||
// Apply same order to all languages
|
||||
const posts = {}
|
||||
const meta = {}
|
||||
|
||||
for (const lang of Object.keys(titles)) {
|
||||
posts[lang] = {}
|
||||
for (const post of ordered) {
|
||||
posts[lang][post.s] = { t: post.t }
|
||||
if (lang === 'en') meta[post.s] = { m: post.m, d: post.o, i: post.i }
|
||||
}
|
||||
}
|
||||
|
||||
return { posts, meta }
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads all newsletter posts, titles and order
|
||||
*/
|
||||
const loadNewsletter = async () => {
|
||||
const titles = await loadFolderFrontmatter('title', 'org', 'newsletter')
|
||||
// Order is the same for all languages, so only grab EN files
|
||||
const order = await loadFolderFrontmatter('edition', 'org', 'newsletter', false, 'en')
|
||||
|
||||
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(',')} }`
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Write out a single prebuild file
|
||||
*/
|
||||
const writeFile = async (filename, exportname, site, content) => {
|
||||
fs.writeFileSync(
|
||||
path.resolve('..', site, 'prebuild', `${filename}.mjs`),
|
||||
`${header}export const ${exportname} = ${JSON.stringify(content)}`
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* 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: { posts: await loadNewsletter() },
|
||||
}
|
||||
await writeFiles('blog', 'org', store.posts.blog.posts)
|
||||
await writeFiles('showcase', 'org', store.posts.showcase.posts)
|
||||
await writeFiles('newsletter', 'org', store.posts.newsletter)
|
||||
await writeFile('blog-meta', 'meta', 'org', store.posts.blog.meta)
|
||||
await writeFile('showcase-meta', 'meta', 'org', store.posts.showcase.meta)
|
||||
}
|
|
@ -1,73 +1,125 @@
|
|||
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 { extendSiteNav as dev } from './sitenav-dev.mjs'
|
||||
import { extendSiteNav as org } from './sitenav-org.mjs'
|
||||
import { pageHasChildren } from '../utils.mjs'
|
||||
import { header } from './shared.mjs'
|
||||
|
||||
// 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 = {}
|
||||
const extendNav = { dev, org }
|
||||
|
||||
/*
|
||||
* 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, postPages, 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 all = {
|
||||
sitenav: '',
|
||||
}
|
||||
const locales = []
|
||||
for (const lang in docs) {
|
||||
locales.push(lang)
|
||||
sitenav[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 postPages) {
|
||||
set(nav, [lang, type], {
|
||||
t: translations[type] || type,
|
||||
s: type,
|
||||
})
|
||||
|
||||
for (const page in postPages[type][lang]) {
|
||||
const pageData = postPages[type][lang][page]
|
||||
const chunks = page.split('/')
|
||||
set(nav, [lang, ...chunks], {
|
||||
t: pageData.t,
|
||||
s: page,
|
||||
o: pageData.o,
|
||||
})
|
||||
// Handle posts
|
||||
if (posts) {
|
||||
for (const type in posts) {
|
||||
for (const [slug, post] of Object.entries(posts[type].posts[lang])) {
|
||||
set(sitenav, [lang, ...slug.split('/')], { t: post.t, o: post.o, s: slug })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add imports for umbrella file
|
||||
all.sitenav += `import { siteNav as ${lang} } from './sitenav.${lang}.mjs'` + '\n'
|
||||
|
||||
// Extend navigation if there's a method for that
|
||||
if (extendNav[site]) sitenav[lang] = extendNav[site](sitenav[lang], lang)
|
||||
|
||||
// Write out navigation object
|
||||
fs.writeFileSync(
|
||||
path.resolve('..', site, 'prebuild', `sitenav.${lang}.mjs`),
|
||||
`${header}export const siteNav = ${JSON.stringify(sitenav[lang])}`
|
||||
)
|
||||
|
||||
/*
|
||||
* Since slugs are language-agnostic, we only need to create a slug lookup tables
|
||||
* once, for which we'll use the EN locale as that one is always present
|
||||
*/
|
||||
if (lang === 'en') {
|
||||
const sluglut = orderedSlugLut(sitenav[lang])
|
||||
// Write out slug lookup table (sluglut)
|
||||
fs.writeFileSync(
|
||||
path.resolve('..', site, 'prebuild', `sluglut.mjs`),
|
||||
`${header}export const slugLut = ${JSON.stringify(sluglut)}`
|
||||
)
|
||||
store.navigation = { sluglut }
|
||||
}
|
||||
}
|
||||
|
||||
// Write umbrella siteNav file
|
||||
fs.writeFileSync(
|
||||
path.resolve('..', site, 'prebuild', `navigation.mjs`),
|
||||
`export const prebuildNavigation = ${JSON.stringify(nav, null, 2)}`
|
||||
path.resolve('..', site, 'prebuild', `sitenav.mjs`),
|
||||
`${header}${all.sitenav}export const siteNav = { ${locales.join(',')} }`
|
||||
)
|
||||
|
||||
return true
|
||||
// Update the store
|
||||
store.navigation.sitenav = sitenav
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import { loadMdxForPrebuild, header } from './docs.mjs'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
|
||||
const types = ['blog', 'showcase', 'newsletter']
|
||||
|
||||
export const prebuildPosts = async (site) => {
|
||||
if (site !== 'org') return {}
|
||||
|
||||
// Languages
|
||||
const locales = ['en', 'fr', 'es', 'nl', 'de', 'uk']
|
||||
const results = await Promise.all(
|
||||
types.map((t) =>
|
||||
loadMdxForPrebuild(site, path.resolve('..', '..', 'markdown', site, t), locales)
|
||||
)
|
||||
)
|
||||
|
||||
const writeOps = []
|
||||
const pages = {}
|
||||
for (var i = 0; i < types.length; i++) {
|
||||
const sorted = {}
|
||||
const resultPages = results[i]
|
||||
|
||||
pages[types[i]] = resultPages
|
||||
for (const lang in resultPages) {
|
||||
sorted[lang] = Object.keys(resultPages[lang]).sort(
|
||||
(a, b) => resultPages[lang][a].o - resultPages[lang][b].o
|
||||
)
|
||||
// get rid of the index page
|
||||
sorted[lang].shift()
|
||||
}
|
||||
|
||||
writeOps.push(
|
||||
fs.writeFile(
|
||||
path.resolve('..', site, 'prebuild', `${types[i]}-paths.mjs`),
|
||||
`${header}export const order = ${JSON.stringify(
|
||||
sorted,
|
||||
null,
|
||||
2
|
||||
)}\nexport const postInfo = ${JSON.stringify(resultPages, null, 2)}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(writeOps)
|
||||
|
||||
return pages
|
||||
}
|
91
sites/shared/prebuild/runner.mjs
Normal file
91
sites/shared/prebuild/runner.mjs
Normal file
|
@ -0,0 +1,91 @@
|
|||
// 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 { prebuildDesigns as designs } from './designs.mjs'
|
||||
import { prebuildFavicon as favicon } from './favicon.mjs'
|
||||
import { prebuildCrowdin as crowdin } from './crowdin.mjs'
|
||||
//import { prebuildLab as lab} from './lab.mjs'
|
||||
//import { prebuildOgImages as ogImages } from './og/index.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()
|
||||
}
|
8
sites/shared/prebuild/shared.mjs
Normal file
8
sites/shared/prebuild/shared.mjs
Normal file
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* A header to include in auto-generated 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.
|
||||
*/
|
||||
`
|
54
sites/shared/prebuild/sitenav-dev.mjs
Normal file
54
sites/shared/prebuild/sitenav-dev.mjs
Normal file
|
@ -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) => {
|
||||
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
|
||||
}
|
270
sites/shared/prebuild/sitenav-org.mjs
Normal file
270
sites/shared/prebuild/sitenav-org.mjs
Normal file
|
@ -0,0 +1,270 @@
|
|||
import { freeSewingConfig as conf } from '../config/freesewing.config.mjs'
|
||||
import { designs, tags } from '../config/designs.mjs'
|
||||
// Translation via i18next directly
|
||||
import i18next from 'i18next'
|
||||
// Actual translations for various languages
|
||||
// EN
|
||||
import accountEn from '../../org/public/locales/en/sections.json' assert { type: 'json' }
|
||||
import designsEn from '../../org/public/locales/en/sections.json' assert { type: 'json' }
|
||||
import sectionsEn from '../../org/public/locales/en/sections.json' assert { type: 'json' }
|
||||
import tagsEn from '../../org/public/locales/en/sections.json' assert { type: 'json' }
|
||||
// DE
|
||||
import accountDe from '../../org/public/locales/de/sections.json' assert { type: 'json' }
|
||||
import designsDe from '../../org/public/locales/de/sections.json' assert { type: 'json' }
|
||||
import sectionsDe from '../../org/public/locales/de/sections.json' assert { type: 'json' }
|
||||
import tagsDe from '../../org/public/locales/de/sections.json' assert { type: 'json' }
|
||||
// ES
|
||||
import accountEs from '../../org/public/locales/es/sections.json' assert { type: 'json' }
|
||||
import designsEs from '../../org/public/locales/es/sections.json' assert { type: 'json' }
|
||||
import sectionsEs from '../../org/public/locales/es/sections.json' assert { type: 'json' }
|
||||
import tagsEs from '../../org/public/locales/es/sections.json' assert { type: 'json' }
|
||||
// FR
|
||||
import accountFr from '../../org/public/locales/fr/sections.json' assert { type: 'json' }
|
||||
import designsFr from '../../org/public/locales/fr/sections.json' assert { type: 'json' }
|
||||
import sectionsFr from '../../org/public/locales/fr/sections.json' assert { type: 'json' }
|
||||
import tagsFr from '../../org/public/locales/fr/sections.json' assert { type: 'json' }
|
||||
// NL
|
||||
import accountNl from '../../org/public/locales/nl/sections.json' assert { type: 'json' }
|
||||
import designsNl from '../../org/public/locales/nl/sections.json' assert { type: 'json' }
|
||||
import sectionsNl from '../../org/public/locales/nl/sections.json' assert { type: 'json' }
|
||||
import tagsNl from '../../org/public/locales/nl/sections.json' assert { type: 'json' }
|
||||
// UK
|
||||
import accountUk from '../../org/public/locales/uk/sections.json' assert { type: 'json' }
|
||||
import designsUk from '../../org/public/locales/uk/sections.json' assert { type: 'json' }
|
||||
import sectionsUk from '../../org/public/locales/uk/sections.json' assert { type: 'json' }
|
||||
import tagsUk from '../../org/public/locales/uk/sections.json' assert { type: 'json' }
|
||||
|
||||
/*
|
||||
* Construct an object we can load the translations from
|
||||
*/
|
||||
const translations = {
|
||||
en: {
|
||||
account: accountEn,
|
||||
design: designsEn,
|
||||
sections: sectionsEn,
|
||||
tags: tagsEn,
|
||||
},
|
||||
de: {
|
||||
account: accountDe,
|
||||
design: designsDe,
|
||||
sections: sectionsDe,
|
||||
tags: tagsDe,
|
||||
},
|
||||
es: {
|
||||
account: accountEs,
|
||||
design: designsEs,
|
||||
sections: sectionsEs,
|
||||
tags: tagsEs,
|
||||
},
|
||||
fr: {
|
||||
account: accountFr,
|
||||
design: designsFr,
|
||||
sections: sectionsFr,
|
||||
tags: tagsFr,
|
||||
},
|
||||
nl: {
|
||||
account: accountNl,
|
||||
design: designsNl,
|
||||
sections: sectionsNl,
|
||||
tags: tagsNl,
|
||||
},
|
||||
uk: {
|
||||
account: accountUk,
|
||||
design: designsUk,
|
||||
sections: sectionsUk,
|
||||
tags: tagsUk,
|
||||
},
|
||||
}
|
||||
|
||||
/* 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) => {
|
||||
const resources = {}
|
||||
resources[lang] = translations[lang]
|
||||
i18next.init({
|
||||
lng: lang,
|
||||
resources,
|
||||
})
|
||||
const { t } = i18next
|
||||
|
||||
const addThese = {
|
||||
blog: {
|
||||
m: 1,
|
||||
s: 'blog',
|
||||
t: t('sections:blog'),
|
||||
n: 1,
|
||||
},
|
||||
showcase: {
|
||||
m: 1,
|
||||
s: 'showcase',
|
||||
t: t('sections:showcase'),
|
||||
n: 1,
|
||||
},
|
||||
docs: {
|
||||
m: 1,
|
||||
s: 'docs',
|
||||
t: t('sections:docs'),
|
||||
},
|
||||
newsletter: {
|
||||
s: 'newsletter',
|
||||
t: t('sections:newsletter'),
|
||||
_: 1,
|
||||
},
|
||||
designs: {
|
||||
m: 1,
|
||||
s: 'designs',
|
||||
t: t('sections:designs'),
|
||||
n: 1,
|
||||
tags: {
|
||||
_: 1,
|
||||
s: 'designs/tags',
|
||||
t: t('design:tags'),
|
||||
o: 'aaa',
|
||||
},
|
||||
},
|
||||
patterns: {
|
||||
m: 1,
|
||||
s: 'patterns',
|
||||
t: t('sections:patterns'),
|
||||
},
|
||||
sets: {
|
||||
m: 1,
|
||||
s: 'sets',
|
||||
t: t('sections:sets'),
|
||||
},
|
||||
community: {
|
||||
m: 1,
|
||||
s: 'community',
|
||||
t: t('sections:community'),
|
||||
},
|
||||
account: {
|
||||
m: 1,
|
||||
s: 'account',
|
||||
t: t('sections:account'),
|
||||
n: 1,
|
||||
reload: {
|
||||
s: `account/reload`,
|
||||
t: t(`account:reload`),
|
||||
},
|
||||
},
|
||||
// Top-level pages that are not in the sections menu
|
||||
apikeys: {
|
||||
_: 1,
|
||||
s: 'apikeys',
|
||||
h: 1,
|
||||
t: t('apikeys'),
|
||||
},
|
||||
curate: {
|
||||
s: 'curate',
|
||||
h: 1,
|
||||
t: t('curate'),
|
||||
sets: {
|
||||
t: t('curateSets'),
|
||||
s: 'curate/sets',
|
||||
},
|
||||
},
|
||||
new: {
|
||||
m: 1,
|
||||
s: 'new',
|
||||
h: 1,
|
||||
t: t('sections:new'),
|
||||
apikey: {
|
||||
c: conf.account.fields.developer.apikeys,
|
||||
s: 'new/apikey',
|
||||
t: t('newApikey'),
|
||||
o: 30,
|
||||
},
|
||||
pattern: {
|
||||
t: t('patternNew'),
|
||||
s: 'new/pattern',
|
||||
o: 10,
|
||||
},
|
||||
set: {
|
||||
t: t('newSet'),
|
||||
s: 'new/set',
|
||||
0: 20,
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
s: 'profile',
|
||||
h: 1,
|
||||
t: t('yourProfile'),
|
||||
},
|
||||
translation: {
|
||||
s: 'translation',
|
||||
h: 1,
|
||||
t: t('translation'),
|
||||
join: {
|
||||
t: t('translation:joinATranslationTeam'),
|
||||
s: 'translation',
|
||||
},
|
||||
'suggest-language': {
|
||||
t: t('translation:suggestLanguage'),
|
||||
s: 'translation',
|
||||
},
|
||||
},
|
||||
sitemap: {
|
||||
s: 'sitemap',
|
||||
h: 1,
|
||||
t: t('sitemap'),
|
||||
},
|
||||
// Not translated, this is a developer page
|
||||
typography: {
|
||||
s: 'typography',
|
||||
h: 1,
|
||||
t: 'Typography',
|
||||
},
|
||||
}
|
||||
|
||||
for (const section in conf.account.fields) {
|
||||
for (const [field, controlScore] of Object.entries(conf.account.fields[section])) {
|
||||
addThese.account[field] = {
|
||||
s: `account/${field}`,
|
||||
t: t(`account:${field}`),
|
||||
c: controlScore,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const design in designs) {
|
||||
// addThese.designs[design] = {
|
||||
// t: t(`designs:${design}.t`),
|
||||
// s: `designs/${design}`,
|
||||
// }
|
||||
addThese.new.pattern[design] = {
|
||||
s: `new/${design}`,
|
||||
t: t(`account:generateANewThing`, { thing: t(`designs:${design}.t`) }),
|
||||
}
|
||||
}
|
||||
|
||||
for (const tag of tags) {
|
||||
addThese.designs.tags[tag] = {
|
||||
s: `designs/tags/${tag}`,
|
||||
t: t(`tags:${tag}`),
|
||||
}
|
||||
}
|
||||
|
||||
// Set order on main sections
|
||||
addThese.designs.o = 10
|
||||
addThese.docs.o = 20
|
||||
addThese.blog.o = 30
|
||||
addThese.showcase.o = 40
|
||||
addThese.community.o = 50
|
||||
addThese.patterns.o = 60
|
||||
addThese.sets.o = 70
|
||||
addThese.account.o = 80
|
||||
addThese.new.o = 90
|
||||
|
||||
return { ...pages, ...addThese }
|
||||
}
|
|
@ -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
|
||||
|
|
22
yarn.lock
22
yarn.lock
|
@ -14672,6 +14672,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"
|
||||
|
@ -18341,6 +18356,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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue