diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index fecd3e9ac2..16cded506c 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -22,7 +22,7 @@ path in a comment names what a page used to be, not a file that still exists. other repositories at build time (product docs, the Blueprint Library) - **Files served as-is**: `nuxt/public/`, a tracked asset tree rather than build output - **CSS**: two Tailwind v4 builds on every page: `nuxt/assets/css/style.css`, compiled by PostCSS to `nuxt/public/css/style.css`, and `nuxt/assets/css/theme.css`, bundled by Nuxt and loaded second -- **Search**: Algolia (`scripts/index-algolia.js`), indexed from the built output +- **Search**: Algolia (`scripts/index-algolia.mjs`), indexed from the built output - **Hosting**: Netlify, from `nuxt/dist` ### Production build order @@ -81,8 +81,7 @@ nuxt/ ├── composables/ ├── utils/ └── pages/ -scripts/ # sync_docs.mjs, sync_blueprints.mjs, their watchers, index-algolia.js -lib/ # search-index.js, shared with scripts/index-algolia.js +scripts/ # sync_docs.mjs, sync_blueprints.mjs, their watchers, index-algolia.mjs ``` --- diff --git a/.claude/launch.json b/.claude/launch.json index 09dc658308..56f9a49471 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -4,7 +4,7 @@ { "name": "nuxt", "runtimeExecutable": "npm", - "runtimeArgs": ["run", "--workspace=nuxt", "dev"], + "runtimeArgs": ["run", "dev:nuxt"], "port": 3000, "autoPort": true } diff --git a/.gitignore b/.gitignore index f878a98dc9..452d0d24c3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,39 @@ .DS_Store node_modules +.env + +# Nuxt build output +nuxt/.nuxt/ +nuxt/.output/ +nuxt/dist/ +nuxt/.data/ + +# Local Netlify folder +.netlify + +# Synced at build time by nuxt/modules/docs-source.ts (from FlowFuse/flowfuse) and +# nuxt/modules/blueprints-source.ts (from FlowFuse/blueprint-library). nuxt/content/docs/ -# Synced from FlowFuse/blueprint-library by nuxt/modules/blueprints-source.ts. +nuxt/public/docs/ nuxt/content/blueprints/ +nuxt/public/blueprints/ + +# nuxt/public/ is a tracked asset tree. Only these parts of it are generated during a build: +nuxt/public/css/ +nuxt/public/js/cc.min.js +nuxt/public/js/flowrenderer.min.js -# Left in existing checkouts by the retired Eleventy build: its output and its -# eleventy-fetch cache. Nothing writes these any more. +# Left in existing checkouts by the retired Eleventy build: its output, its eleventy-fetch +# cache, and what it wrote into nuxt/public/. Nothing writes these any more. _site .cache +nuxt/public/img/ +nuxt/public/_includes/ +nuxt/public/sitemap-legacy.xml +nuxt/public/js/ai-expert-modal.js +nuxt/public/js/hm-promo-banner.js # Local development config .vscode/ -.env - -# Local Netlify folder -.netlify - - -deno.lock \ No newline at end of file +deno.lock diff --git a/README.md b/README.md index cde9818cfd..5642aa8fcf 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ schedule to pick up blueprint-library changes — either way it just calls a Net ## Repository structure -This repository is an **npm workspace**. The whole site is one [Nuxt 4](https://nuxt.com/) application in `nuxt/`: +The whole site is one [Nuxt 4](https://nuxt.com/) application in `nuxt/`, with a single `package.json` at the repository root: | Directory | Purpose | |-----------|---------| @@ -28,7 +28,7 @@ This repository is an **npm workspace**. The whole site is one [Nuxt 4](https:// | `nuxt/assets` | Stylesheets, the two icon sets (`icons/` for ``, `nav-icons/` for ``), the cookie-consent source | | `nuxt/lib`, `nuxt/server/lib` | Plain-JS helpers with `node --test` unit tests | | `scripts/` | Build-time syncs for the two external content sources, and the Algolia indexer | -| *(root)* | The workspace: npm scripts, Tailwind and PostCSS config, Netlify config | +| *(root)* | `package.json` (dependencies and npm scripts), PostCSS config, Netlify config | The site was previously generated by [Eleventy](https://www.11ty.dev/) and was migrated to Nuxt page group by page group, using the [Strangler Fig pattern](https://martinfowler.com/bliki/StranglerFigApplication.html). @@ -55,7 +55,7 @@ is a reference to what a page used to be. ## Running locally -Clone the repository, then install all dependencies (workspace packages are included automatically): +Clone the repository, then install all dependencies: ```bash npm install @@ -127,7 +127,7 @@ Every build resolves blueprints in this order, and logs which one it used: | 3 | A clone, authenticated with a minted GitHub App installation token (`GH_BOT_APP_ID`/`GH_BOT_APP_KEY`) | Nothing above applied, and those env vars are set. This is what Netlify production deploys use — `blueprint-library` is private, so this can't clone anonymously the way docs does. | | 4 | Skipped | Nothing above applied. Matches the previous behaviour for contributors without access to the private repo. | -`npm run blueprints` runs that resolution on its own, writing `nuxt/content/blueprints` and `nuxt/public/blueprints`, both gitignored. `npm run dev` re-runs it whenever the resolved source changes (`scripts/watch_blueprints.js`), but only when it found a source to watch in the first place — no sibling checkout means no watching. +`npm run blueprints` runs that resolution on its own, writing `nuxt/content/blueprints` and `nuxt/public/blueprints`, both gitignored. `npm run dev` re-runs it whenever the resolved source changes (`scripts/watch_blueprints.mjs`), but only when it found a source to watch in the first place — no sibling checkout means no watching. ## llms.txt diff --git a/nuxt/.gitignore b/nuxt/.gitignore deleted file mode 100644 index 8163279d33..0000000000 --- a/nuxt/.gitignore +++ /dev/null @@ -1,27 +0,0 @@ -# Nuxt build outputs -.nuxt/ -.output/ -dist/ - -# public/ is a tracked asset tree. Only these parts of it are generated during a build: -/public/css/ -/public/js/cc.min.js -/public/js/flowrenderer.min.js -# Synced from FlowFuse/flowfuse by modules/docs-source.ts -/public/docs/ -# Synced from FlowFuse/blueprint-library by modules/blueprints-source.ts -/public/blueprints/ -# Left in existing checkouts by the retired Eleventy build, which wrote its output into -# public/. Nothing writes these any more. -/public/img/ -/public/_includes/ -/public/sitemap-legacy.xml -/public/js/ai-expert-modal.js -/public/js/hm-promo-banner.js - -# Dependencies (hoisted to workspace root) -node_modules/ -.data/ - -# Local Netlify folder -.netlify diff --git a/nuxt/assets/css/style.css b/nuxt/assets/css/style.css index 1d9a7e259d..3431388ac3 100644 --- a/nuxt/assets/css/style.css +++ b/nuxt/assets/css/style.css @@ -1,6 +1,6 @@ @layer vendor, theme, base, components, utilities; -@config "../../../tailwind.config.js"; +@config "../../tailwind.config.js"; @import "tailwindcss"; /* Utility classes for the nav and footer live in this data file rather than in a template, so name it explicitly: automatic source detection skips JSON. */ @@ -117,8 +117,8 @@ @import "./roadmap.css"; @import "./zoom-style.css"; @import "./node-red-style.css"; -@import "../../../node_modules/@flowforge/forge-ui-components/dist/forge-ui-components.css" layer(vendor); -@import "../../../node_modules/vanilla-cookieconsent/dist/cookieconsent.css" layer(components); +@import "@flowforge/forge-ui-components/dist/forge-ui-components.css" layer(vendor); +@import "vanilla-cookieconsent/dist/cookieconsent.css" layer(components); /* forge-ui-components resets bare h1-h3 for its own standalone docs site. Neutralize within the vendor layer itself (source order wins ties inside diff --git a/nuxt/components/AlgoliaSearch.vue b/nuxt/components/AlgoliaSearch.vue index 7cfab36266..a47daaeac0 100644 --- a/nuxt/components/AlgoliaSearch.vue +++ b/nuxt/components/AlgoliaSearch.vue @@ -1,6 +1,6 @@ ` diff --git a/nuxt/lib/redirects-node-red.test.mjs b/nuxt/lib/redirects-node-red.test.mjs index 3ab42580e5..0d26af77fc 100644 --- a/nuxt/lib/redirects-node-red.test.mjs +++ b/nuxt/lib/redirects-node-red.test.mjs @@ -10,12 +10,11 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' +import { join } from 'node:path' import { GUIDES_SOURCE, listGuideFiles } from './guides-sync.mjs' +import { REPO_ROOT as repoRoot } from './repo-root.mjs' -const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '../..') const read = rel => readFileSync(join(repoRoot, rel), 'utf8') const withSlashes = path => '/' + path.split('/').filter(Boolean).join('/') + '/' diff --git a/nuxt/lib/repo-root.mjs b/nuxt/lib/repo-root.mjs new file mode 100644 index 0000000000..55e2967249 --- /dev/null +++ b/nuxt/lib/repo-root.mjs @@ -0,0 +1,20 @@ +import { existsSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +// The root of the checkout: the closest directory at or above `start` that holds +// package-lock.json. Found rather than written as a fixed run of `..`, so a caller keeps +// working when the file it lives in moves up or down a level. Closest rather than +// furthest, so a git worktree nested inside another checkout (.claude/worktrees/) +// resolves to itself and not to the checkout around it. +export function findRepoRoot (start) { + let dir = start + while (!existsSync(join(dir, 'package-lock.json'))) { + const parent = dirname(dir) + if (parent === dir) throw new Error(`No package-lock.json at or above ${start}`) + dir = parent + } + return dir +} + +export const REPO_ROOT = findRepoRoot(dirname(fileURLToPath(import.meta.url))) diff --git a/nuxt/lib/repo-root.test.mjs b/nuxt/lib/repo-root.test.mjs new file mode 100644 index 0000000000..eb98f1d665 --- /dev/null +++ b/nuxt/lib/repo-root.test.mjs @@ -0,0 +1,27 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { findRepoRoot } from './repo-root.mjs' + +test('findRepoRoot returns the closest directory holding package-lock.json', (t) => { + const root = mkdtempSync(join(tmpdir(), 'repo-root-')) + t.after(() => rmSync(root, { recursive: true, force: true })) + // An outer checkout with a worktree nested inside it, as .claude/worktrees/ does. + const worktree = join(root, '.claude', 'worktrees', 'topic') + mkdirSync(join(worktree, 'nuxt', 'lib'), { recursive: true }) + writeFileSync(join(root, 'package-lock.json'), '{}') + writeFileSync(join(worktree, 'package-lock.json'), '{}') + + assert.equal(findRepoRoot(join(worktree, 'nuxt', 'lib')), worktree) + assert.equal(findRepoRoot(worktree), worktree) + assert.equal(findRepoRoot(join(root, '.claude')), root) +}) + +test('findRepoRoot fails loudly when there is no package-lock.json above', (t) => { + const root = mkdtempSync(join(tmpdir(), 'repo-root-')) + t.after(() => rmSync(root, { recursive: true, force: true })) + assert.throws(() => findRepoRoot(root), /No package-lock.json/) +}) diff --git a/nuxt/lib/retired-src.test.mjs b/nuxt/lib/retired-src.test.mjs index 5747a568b1..173026ca43 100644 --- a/nuxt/lib/retired-src.test.mjs +++ b/nuxt/lib/retired-src.test.mjs @@ -4,11 +4,8 @@ // merges cleanly into src/blog/, and the only sign is a page that never appears. import assert from 'node:assert/strict' import { execFileSync } from 'node:child_process' -import { dirname, join } from 'node:path' import { test } from 'node:test' -import { fileURLToPath } from 'node:url' - -const repo = join(dirname(fileURLToPath(import.meta.url)), '..', '..') +import { REPO_ROOT as repo } from './repo-root.mjs' test('nothing is tracked under src/', (t) => { let tracked diff --git a/lib/search-index.js b/nuxt/lib/search-index.mjs similarity index 98% rename from lib/search-index.js rename to nuxt/lib/search-index.mjs index 28a2aaba75..b9988d14c3 100644 --- a/lib/search-index.js +++ b/nuxt/lib/search-index.mjs @@ -1,3 +1,6 @@ +import fs from "node:fs"; +import path from "node:path"; + function decodeEntities(text = "") { return text .replace(/ /gi, " ") @@ -306,8 +309,6 @@ function toUnixTimestampSeconds(value) { } async function listHtmlFiles(rootDir) { - const fs = require("fs"); - const path = require("path"); const files = []; async function walk(currentDir) { const entries = await fs.promises.readdir(currentDir, { withFileTypes: true }); @@ -325,7 +326,6 @@ async function listHtmlFiles(rootDir) { } function outputPathToUrl(outputRoot, outputPath) { - const path = require("path"); const rel = path.relative(outputRoot, outputPath).replace(/\\/g, "/"); if (!rel.endsWith(".html")) return ""; if (rel === "index.html") return "/"; @@ -349,7 +349,7 @@ function extractArticleSection(html) { return extractMetaTag(html, 'property=["\\\']article:section["\\\']').trim(); } -module.exports = { +export { isSearchPage, isSearchUrl, extractHeadingRecords, diff --git a/nuxt/modules/blueprints-source.ts b/nuxt/modules/blueprints-source.ts index 5b49e73ab1..94ec03e45c 100644 --- a/nuxt/modules/blueprints-source.ts +++ b/nuxt/modules/blueprints-source.ts @@ -1,10 +1,11 @@ import { defineNuxtModule, useLogger } from '@nuxt/kit' -import { dirname } from 'node:path' // Lives in nuxt/lib/, not alongside this file: Nuxt auto-registers everything in // nuxt/modules/ as a Nuxt module, so a plain helper there fails the build. // @ts-ignore untyped module, kept as plain JS so `node --test` can run it directly import { syncBlueprints } from '../lib/blueprints-sync.mjs' +// @ts-ignore same +import { findRepoRoot } from '../lib/repo-root.mjs' import { BLUEPRINTS_PAGE_SIZE } from '../composables/useBlueprintList' const logger = useLogger('blueprints-source') @@ -13,7 +14,7 @@ export default defineNuxtModule({ meta: { name: 'blueprints-source' }, async setup (_options, nuxt) { const nuxtRoot = nuxt.options.rootDir - const repoRoot = dirname(nuxtRoot) + const repoRoot = findRepoRoot(nuxtRoot) const { entries } = await syncBlueprints({ repoRoot, nuxtRoot, logger }) diff --git a/nuxt/modules/docs-source.ts b/nuxt/modules/docs-source.ts index db3a8a91b3..eaee29c5ae 100644 --- a/nuxt/modules/docs-source.ts +++ b/nuxt/modules/docs-source.ts @@ -1,6 +1,6 @@ import { defineNuxtModule, extendRouteRules, useLogger } from '@nuxt/kit' import { existsSync, readFileSync, readdirSync } from 'node:fs' -import { join, basename, dirname } from 'node:path' +import { join, basename } from 'node:path' // Lives in nuxt/lib/, not alongside this file: Nuxt auto-registers everything in // nuxt/modules/ as a Nuxt module, so a plain helper there fails the build. @@ -10,6 +10,8 @@ import { syncDocs } from '../lib/docs-sync.mjs' import { syncGuides } from '../lib/guides-sync.mjs' // @ts-ignore same import { docsRedirectRules, prerenderableRoutes } from '../lib/docs-redirects.mjs' +// @ts-ignore same +import { findRepoRoot } from '../lib/repo-root.mjs' const logger = useLogger('docs-source') @@ -34,7 +36,7 @@ export default defineNuxtModule({ const nuxtRoot = nuxt.options.rootDir const contentDocsDir = join(nuxtRoot, 'content', 'docs') - const repoRoot = dirname(nuxtRoot) + const repoRoot = findRepoRoot(nuxtRoot) // Order matters: syncDocs wipes content/docs before writing, so the guides overlaid // from this repo have to land after it, not before. diff --git a/nuxt/nuxt.config.ts b/nuxt/nuxt.config.ts index 96502b3428..3f40c3ec3d 100644 --- a/nuxt/nuxt.config.ts +++ b/nuxt/nuxt.config.ts @@ -636,9 +636,9 @@ export default defineNuxtConfig({ repo: 'website', branch: 'main', branchStrategy: 'feature-branch', - // The Nuxt app is the `nuxt` npm workspace inside the website repo, not the repo - // root, so @nuxt/content's `content/handbook/...` paths need this prefix to match - // the real path Studio commits to via the GitHub Contents API. + // The Nuxt app lives in the website repo's `nuxt/` directory, not the repo root, + // so @nuxt/content's `content/handbook/...` paths need this prefix to match the + // real path Studio commits to via the GitHub Contents API. rootDir: 'nuxt', } }, diff --git a/nuxt/package.json b/nuxt/package.json deleted file mode 100644 index fe709cc552..0000000000 --- a/nuxt/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "flowfuse-website-nuxt", - "version": "0.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "TMPDIR=/tmp nuxt dev", - "build": "nuxt build", - "generate": "nuxt generate", - "preview": "nuxt preview" - }, - "dependencies": { - "@iconify-json/heroicons": "^1.2.3", - "@iconify-json/lucide": "^1.2.123", - "@resvg/resvg-js": "^2.6.2", - "@takumi-rs/core": "^1.8.4", - "markdown-it": "^14.2.0", - "mermaid": "^11.16.1", - "nuxt-llms": "^0.2.0", - "rehype-katex": "^7.0.1", - "remark-math": "^6.0.0", - "sanitize-html": "^2.17.7", - "satori": "^0.26.0", - "unstorage": "^1.17.5" - }, - "devDependencies": { - "@nuxt/content": "^3.13.0", - "@nuxt/image": "^2.0.0", - "@nuxt/ui": "^4.8.2", - "@nuxtjs/seo": "^5.3.12", - "@types/sanitize-html": "^2.16.1", - "better-sqlite3": "^12.10.0", - "nuxt": "^4.5.1", - "nuxt-link-checker": "^5.0.10", - "nuxt-og-image": "^6.7.8", - "nuxt-studio": "file:.local-packages/nuxt-studio-1.7.0.tgz" - } -} diff --git a/nuxt/server/api/__sitemap__/content-urls.get.ts b/nuxt/server/api/__sitemap__/content-urls.get.ts index a41efd99a7..3d54d16059 100644 --- a/nuxt/server/api/__sitemap__/content-urls.get.ts +++ b/nuxt/server/api/__sitemap__/content-urls.get.ts @@ -13,8 +13,8 @@ const toAbsoluteUrl = (event: Parameters[0], path: string) (path.startsWith('http') ? path : `${getSiteConfig(event).url}${path}`) // Runs at prerender time (during `nuxt generate`), inside the git checkout, so walking up -// from cwd to find `.git` is robust whether the build is invoked from the repo root or the -// `nuxt/` workspace - unlike at request time in the deployed function, where no .git exists. +// from cwd to find `.git` is robust wherever in the checkout the build is invoked from - +// unlike at request time in the deployed function, where no .git exists. function findRepoRoot (start: string): string { let dir = start for (let i = 0; i < 6; i++) { diff --git a/nuxt/server/lib/handbookChanges.mjs b/nuxt/server/lib/handbookChanges.mjs index 3945bc61d5..0917ea15e3 100644 --- a/nuxt/server/lib/handbookChanges.mjs +++ b/nuxt/server/lib/handbookChanges.mjs @@ -4,10 +4,12 @@ // week (Monday start), newest week first, so the team can review "last week's // changes" at the Monday review. // -// The handbook used to live under `src/handbook` (11ty) and now lives under -// `nuxt/content/handbook` (@nuxt/content). Both prefixes are scanned so the -// full history survives the Nuxt migration; both map onto the same -// `/handbook/...` URLs the live site serves. +// The handbook used to live under `src/handbook` (11ty), then under +// `nuxt/content/handbook` (@nuxt/content), and moves to `content/handbook` once +// the Nuxt app leaves its subdirectory. Every prefix is scanned so the full history +// survives each move; all of them map onto the same `/handbook/...` URLs the live +// site serves. A commit that only moves a page between two of them is skipped, so a +// move does not list every page as renamed. // // This module deliberately depends only on Node built-ins so it can be // imported by the Nitro server route AND executed standalone for verification. @@ -21,9 +23,10 @@ const REPO_URL = 'https://github.com/FlowFuse/website' // subjects, so subjects containing spaces or pipes parse cleanly. const SEP = '\x01' -// Path prefixes that count as handbook content. Order matters only for display -// labels; both fold onto the same handbook-relative path. -const HANDBOOK_PREFIXES = ['nuxt/content/handbook/', 'src/handbook/'] +// Path prefixes that count as handbook content, newest location first. They are +// history, not the current layout, so a move adds one rather than rewriting them. +// All fold onto the same handbook-relative path. +const HANDBOOK_PREFIXES = ['content/handbook/', 'nuxt/content/handbook/', 'src/handbook/'] // Where the CURRENT version of a page lives, used for title lookups. const CONTENT_ROOT = 'nuxt/content/handbook' @@ -42,6 +45,13 @@ export function toHandbookRel (path) { return rel } +/** True when a rename status only moves a page from one handbook prefix to another. */ +export function isRelocation (status, fromPath, toPath) { + if (!status.startsWith('R')) return false + const rel = toHandbookRel(toPath) + return rel !== null && rel === toHandbookRel(fromPath) +} + /** Map a handbook-relative file path to the URL the site serves (trailing slash, like the rest of the handbook). */ export function relToUrl (rel) { let noExt = rel.replace(/\.(md|njk)$/, '') @@ -150,7 +160,7 @@ export function getHandbookChanges (repoRoot = process.cwd()) { output = execFileSync('git', [ 'log', '--no-merges', '-M', '--name-status', '--date=short', `--pretty=format:${SEP}%H${SEP}%ad${SEP}%s`, - '--', 'nuxt/content/handbook', 'src/handbook' + '--', ...HANDBOOK_PREFIXES.map(prefix => prefix.slice(0, -1)) ], { cwd: repoRoot, maxBuffer: 64 * 1024 * 1024 }).toString() } catch (err) { // Never break the build/render if git is unavailable. @@ -171,6 +181,7 @@ export function getHandbookChanges (repoRoot = process.cwd()) { const parts = line.split('\t') const status = parts[0] const path = status.startsWith('R') || status.startsWith('C') ? parts[2] : parts[1] + if (isRelocation(status, parts[1], path)) continue if (path) cur.files.push({ status, path }) } } diff --git a/nuxt/server/lib/handbookChanges.test.mjs b/nuxt/server/lib/handbookChanges.test.mjs index 623eca8793..0d43c2750c 100644 --- a/nuxt/server/lib/handbookChanges.test.mjs +++ b/nuxt/server/lib/handbookChanges.test.mjs @@ -7,7 +7,7 @@ import { test } from 'node:test' import assert from 'node:assert/strict' import { execFileSync } from 'node:child_process' -import { getHandbookChanges, toHandbookRel, relToUrl, mondayOf, prsFromCommits } from './handbookChanges.mjs' +import { getHandbookChanges, toHandbookRel, relToUrl, mondayOf, prsFromCommits, isRelocation } from './handbookChanges.mjs' // --- pure helpers ----------------------------------------------------------- @@ -48,6 +48,7 @@ test('prsFromCommits dedupes a single commit shared across pages and sorts newes }) test('toHandbookRel keeps tracked handbook pages and strips the prefix', () => { + assert.equal(toHandbookRel('content/handbook/company/values.md'), 'company/values.md') assert.equal(toHandbookRel('nuxt/content/handbook/company/values.md'), 'company/values.md') assert.equal(toHandbookRel('src/handbook/company/values.md'), 'company/values.md') assert.equal(toHandbookRel('src/handbook/engineering/product/features.njk'), 'engineering/product/features.njk') @@ -60,6 +61,14 @@ test('toHandbookRel rejects non-handbook, non-page and excluded files', () => { assert.equal(toHandbookRel('src/handbook/data.json'), null) // other json }) +test('isRelocation recognises a page moved between handbook prefixes', () => { + assert.equal(isRelocation('R100', 'nuxt/content/handbook/company/values.md', 'content/handbook/company/values.md'), true) + assert.equal(isRelocation('R087', 'src/handbook/company/values.md', 'nuxt/content/handbook/company/values.md'), true, 'edited while moving') + assert.equal(isRelocation('R100', 'content/handbook/company/values.md', 'content/handbook/company/principles.md'), false, 'a real rename') + assert.equal(isRelocation('M', 'content/handbook/company/values.md', undefined), false) + assert.equal(isRelocation('R100', 'nuxt/content/ebooks/a.md', 'content/ebooks/a.md'), false, 'not handbook') +}) + test('relToUrl maps file paths to trailing-slash handbook URLs', () => { assert.equal(relToUrl('company/values.md'), '/handbook/company/values/') assert.equal(relToUrl('company/index.md'), '/handbook/company/') @@ -165,27 +174,27 @@ test('the actual latest handbook commit appears in the most recent week', () => const log = execFileSync('git', [ 'log', '--no-merges', '-M', '--name-status', '--date=short', `--pretty=format:${RECORD}%H%x1f%ad%x1f%s`, - '--', 'nuxt/content/handbook', 'src/handbook' + '--', 'content/handbook', 'nuxt/content/handbook', 'src/handbook' ], { cwd: process.cwd(), maxBuffer: 64 * 1024 * 1024 }).toString() + // A commit that only moves pages between handbook prefixes is skipped by the module, + // so it is left out here too, or the move itself would be "the latest change". const parsed = log.split(RECORD).filter(chunk => chunk.trim()).map(chunk => { - const [header, ...files] = chunk.split('\n').filter(Boolean) + const [header, ...fileLines] = chunk.split('\n').filter(Boolean) const [sha, date, subject] = header.split('\x1f') - return { sha, date, subject, files } - }) - assert.ok(parsed.length > 0, 'git reports at least one handbook commit') + const urls = fileLines.map(line => line.split('\t')) + .filter(parts => !isRelocation(parts[0], parts[1], parts[2])) + .map(parts => parts[0].startsWith('R') || parts[0].startsWith('C') ? parts[2] : parts[1]) + .map(toHandbookRel).filter(Boolean).map(relToUrl) + return { sha, date, subject, urls } + }).filter(commit => commit.urls.length > 0) + assert.ok(parsed.length > 0, 'git reports at least one commit touching a tracked handbook page') // Ties on the same date are fine: any commit from that date lands in the same week, // and the per-page assertions below only need one of them. const newest = parsed.reduce((a, b) => (b.date > a.date ? b : a)) - const { sha, date, subject } = newest - const fileLines = newest.files + const { sha, date, subject, urls } = newest const prMatch = subject.match(/\(#(\d+)\)\s*$/) - const files = fileLines.map(line => line.split('\t')).map(parts => - parts[0].startsWith('R') || parts[0].startsWith('C') ? parts[2] : parts[1] - ) - const urls = files.map(toHandbookRel).filter(Boolean).map(relToUrl) - assert.ok(urls.length > 0, 'the latest handbook commit touches at least one tracked page') const weeks = getHandbookChanges(process.cwd()) const recent = weeks[0] diff --git a/tailwind.config.js b/nuxt/tailwind.config.js similarity index 89% rename from tailwind.config.js rename to nuxt/tailwind.config.js index f712f6df2a..41fc187dcb 100644 --- a/tailwind.config.js +++ b/nuxt/tailwind.config.js @@ -1,4 +1,4 @@ -module.exports = { +export default { theme: { extend: { typography: { diff --git a/package-lock.json b/package-lock.json index 00abd74982..6002abf6e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,19 +8,30 @@ "name": "flowfuse-website", "version": "1.0.0", "license": "Apache-2.0", - "workspaces": [ - "nuxt" - ], "dependencies": { "@flowfuse/flow-renderer": "^0.5.0", + "@iconify-json/heroicons": "^1.2.3", + "@iconify-json/lucide": "^1.2.123", "@netlify/blobs": "^10.7.9", "@octokit/auth-app": "^8.3.0", "algoliasearch": "^4.25.3", "js-yaml": "^4.3.2", - "markdown-it-attrs": "^4.1.6" + "markdown-it": "^14.2.0", + "markdown-it-attrs": "^4.1.6", + "mermaid": "^11.16.1", + "nuxt-llms": "^0.2.0", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", + "sanitize-html": "^2.17.7", + "satori": "^0.26.0", + "unstorage": "^1.17.5" }, "devDependencies": { "@flowforge/forge-ui-components": "^0.2.2", + "@nuxt/content": "^3.13.0", + "@nuxt/image": "^2.0.0", + "@nuxt/ui": "^4.8.2", + "@nuxtjs/seo": "^5.3.12", "@parcel/watcher": "2.5.6", "@resvg/resvg-js": "2.6.2", "@tailwindcss/postcss": "^4.0.0", @@ -28,6 +39,8 @@ "@takumi-rs/core": "1.8.4", "@types/markdown-it-attrs": "^4.1.3", "@types/markdown-it-footnote": "^3.0.4", + "@types/sanitize-html": "^2.16.1", + "better-sqlite3": "^12.10.0", "chokidar": "^3.6.0", "del-cli": "^5.0.0", "dotenv-cli": "^7.4.4", @@ -37,6 +50,10 @@ "netlify-plugin-cache": "^1.0.3", "nodemon": "^2.0.20", "npm-run-all2": "^7.0.1", + "nuxt": "^4.5.1", + "nuxt-link-checker": "^5.0.10", + "nuxt-og-image": "^6.7.8", + "nuxt-studio": "file:nuxt/.local-packages/nuxt-studio-1.7.0.tgz", "oxc-minify": "0.131.0", "oxc-parser": "0.134.0", "oxc-transform": "0.131.0", @@ -912,7 +929,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -924,7 +940,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -935,7 +950,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -962,7 +976,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -979,7 +992,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -996,7 +1008,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1013,7 +1024,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1030,7 +1040,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1047,7 +1056,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1064,7 +1072,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1081,7 +1088,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1098,7 +1104,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1115,7 +1120,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1132,7 +1136,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1149,7 +1152,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1166,7 +1168,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1183,7 +1184,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1200,7 +1200,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1217,7 +1216,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1234,7 +1232,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1251,7 +1248,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1268,7 +1264,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1285,7 +1280,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1302,7 +1296,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1319,7 +1312,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1336,7 +1328,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1353,7 +1344,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1370,7 +1360,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1387,7 +1376,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2436,7 +2424,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4449,7 +4436,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4466,7 +4452,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4483,7 +4468,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4500,7 +4484,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4517,7 +4500,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4534,7 +4516,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4551,7 +4532,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4568,7 +4548,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4585,7 +4564,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4602,7 +4580,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4619,7 +4596,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4636,7 +4612,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4653,7 +4628,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4670,7 +4644,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4687,7 +4660,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4704,7 +4676,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4721,7 +4692,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -4740,7 +4710,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4757,7 +4726,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -4774,7 +4742,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -5550,6 +5517,7 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz", "integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==", + "dev": true, "license": "MPL-2.0", "engines": { "node": ">= 10" @@ -5576,6 +5544,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5592,6 +5561,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5608,6 +5578,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5624,6 +5595,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5640,6 +5612,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5656,6 +5629,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5672,6 +5646,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5688,6 +5663,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5704,6 +5680,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5720,6 +5697,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5736,6 +5714,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -5752,6 +5731,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -6181,7 +6161,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6195,7 +6174,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6209,7 +6187,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6223,7 +6200,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6237,7 +6213,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6251,7 +6226,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6265,7 +6239,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6279,7 +6252,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6293,7 +6265,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6307,7 +6278,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6321,7 +6291,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6335,7 +6304,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6349,7 +6317,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6363,7 +6330,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6377,7 +6343,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6391,7 +6356,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6405,7 +6369,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6419,7 +6382,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6433,7 +6395,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6447,7 +6408,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6461,7 +6421,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6475,7 +6434,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6489,7 +6447,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6503,7 +6460,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -6517,7 +6473,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -7118,6 +7073,7 @@ "version": "1.8.4", "resolved": "https://registry.npmjs.org/@takumi-rs/core/-/core-1.8.4.tgz", "integrity": "sha512-ogGSVL0+3sH1dUmcyBA5xzHAMOjD1BmB3APt/Zhw0OJy4HSj06CNXrntDAsfZ3+0BRj6PR1SfpRcLJuku671Zw==", + "dev": true, "license": "(MIT OR Apache-2.0)", "dependencies": { "@takumi-rs/helpers": "1.8.4" @@ -7143,6 +7099,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "(MIT OR Apache-2.0)", "optional": true, "os": [ @@ -7159,6 +7116,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "(MIT OR Apache-2.0)", "optional": true, "os": [ @@ -7175,6 +7133,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "(MIT OR Apache-2.0)", "optional": true, "os": [ @@ -7191,6 +7150,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "(MIT OR Apache-2.0)", "optional": true, "os": [ @@ -7207,6 +7167,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "(MIT OR Apache-2.0)", "optional": true, "os": [ @@ -7223,6 +7184,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "(MIT OR Apache-2.0)", "optional": true, "os": [ @@ -7239,6 +7201,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "(MIT OR Apache-2.0)", "optional": true, "os": [ @@ -7255,6 +7218,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "(MIT OR Apache-2.0)", "optional": true, "os": [ @@ -7268,6 +7232,7 @@ "version": "1.8.4", "resolved": "https://registry.npmjs.org/@takumi-rs/helpers/-/helpers-1.8.4.tgz", "integrity": "sha512-HhQnxc5wj/mguTaZoAvMW2k1JShI3BI0os5/2x+FACvgJrpVScYBUvM7uCtGOC1u2CRJYOX6V0Pu59oT3C2rEg==", + "dev": true, "license": "(MIT OR Apache-2.0)", "peerDependencies": { "react": "^19.2.5", @@ -7967,7 +7932,6 @@ "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -10506,18 +10470,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -12641,10 +12593,6 @@ "node": ">=18" } }, - "node_modules/flowfuse-website-nuxt": { - "resolved": "nuxt", - "link": true - }, "node_modules/fnv1a-64": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/fnv1a-64/-/fnv1a-64-0.1.2.tgz", @@ -15617,9 +15565,9 @@ "license": "MIT" }, "node_modules/linkify-it": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz", - "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", "funding": [ { "type": "github", @@ -15631,9 +15579,8 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "uc.micro": "^3.0.0" + "uc.micro": "^2.0.0" } }, "node_modules/linkifyjs": { @@ -15881,37 +15828,10 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/markdown-exit/node_modules/linkify-it": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", - "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/markdown-exit/node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "dev": true, - "license": "MIT" - }, "node_modules/markdown-it": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.0.tgz", - "integrity": "sha512-Lf8ajvVNdRpzSNB4VegxNy7gjs8gU35l4b4+ET49LrQC5PKYwLZ72u60LeJ9gv3qiaesuYjJWCyVeQmv/QWKQw==", + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", + "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", "funding": [ { "type": "github", @@ -15923,14 +15843,13 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "argparse": "^3.0.0", - "entities": "^8.0.0", - "linkify-it": "^6.0.0", - "mdurl": "^2.1.0", + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", "punycode.js": "^2.3.1", - "uc.micro": "^3.0.0" + "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" @@ -15966,31 +15885,13 @@ "dev": true, "license": "MIT" }, - "node_modules/markdown-it/node_modules/argparse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.0.tgz", - "integrity": "sha512-BOp5NMrHqKxmq/OLr+clzzrRxgOKSLkcjmkWuChp7Irqwn4s74WjOBPIgWfA/HMcBnVkZ5XEuf9uUqzlpfCQ6A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "Python-2.0", - "peer": true - }, "node_modules/markdown-it/node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", - "peer": true, "engines": { - "node": ">=20.19.0" + "node": ">=0.12" }, "funding": { "url": "https://github.com/fb55/entities?sponsor=1" @@ -23883,11 +23784,10 @@ } }, "node_modules/uc.micro": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz", - "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==", - "license": "MIT", - "peer": true + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" }, "node_modules/ufo": { "version": "1.6.4", @@ -26063,100 +25963,6 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } - }, - "nuxt": { - "name": "flowfuse-website-nuxt", - "version": "0.0.0", - "dependencies": { - "@iconify-json/heroicons": "^1.2.3", - "@iconify-json/lucide": "^1.2.123", - "@resvg/resvg-js": "^2.6.2", - "@takumi-rs/core": "^1.8.4", - "markdown-it": "^14.2.0", - "mermaid": "^11.16.1", - "nuxt-llms": "^0.2.0", - "rehype-katex": "^7.0.1", - "remark-math": "^6.0.0", - "sanitize-html": "^2.17.7", - "satori": "^0.26.0", - "unstorage": "^1.17.5" - }, - "devDependencies": { - "@nuxt/content": "^3.13.0", - "@nuxt/image": "^2.0.0", - "@nuxt/ui": "^4.8.2", - "@nuxtjs/seo": "^5.3.12", - "@types/sanitize-html": "^2.16.1", - "better-sqlite3": "^12.10.0", - "nuxt": "^4.5.1", - "nuxt-link-checker": "^5.0.10", - "nuxt-og-image": "^6.7.8", - "nuxt-studio": "file:.local-packages/nuxt-studio-1.7.0.tgz" - } - }, - "nuxt/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "nuxt/node_modules/linkify-it": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", - "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "nuxt/node_modules/markdown-it": { - "version": "14.3.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", - "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/markdown-it" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.5.0", - "linkify-it": "^5.0.2", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "nuxt/node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT" } } } diff --git a/package.json b/package.json index 48a3d1adc0..5bce041af3 100644 --- a/package.json +++ b/package.json @@ -6,28 +6,30 @@ "keywords": [], "license": "Apache-2.0", "author": "Nick O'Leary", - "workspaces": [ - "nuxt" - ], + "type": "module", "scripts": { "test": "node --test nuxt/server/lib/*.test.mjs nuxt/lib/*.test.mjs", "dev": "npm-run-all2 --parallel dev:*", - "dev:blueprints": "node scripts/watch_blueprints.js", + "dev:blueprints": "node scripts/watch_blueprints.mjs", "dev:docs": "node scripts/watch_docs.mjs", - "dev:postcss-nuxt": "dotenv -v TAILWIND_MODE=watch -- npx postcss ./nuxt/assets/css/style.css -o ./nuxt/public/css/style.css --config ./postcss.config.js -w", + "dev:postcss-nuxt": "dotenv -v TAILWIND_MODE=watch -- npx postcss ./nuxt/assets/css/style.css -o ./nuxt/public/css/style.css --config ./postcss.config.cjs -w", "blueprints": "node scripts/sync_blueprints.mjs", "docs": "node scripts/sync_docs.mjs", - "index:algolia": "node scripts/index-algolia.js", - "dev:nuxt": "dotenv -- npm run dev --workspace=nuxt", + "index:algolia": "node scripts/index-algolia.mjs", + "dev:nuxt": "dotenv -v TMPDIR=/tmp -- nuxt dev nuxt", "clean:nuxt": "npx del-cli 'nuxt/public/css' 'nuxt/public/js/cc.min.js' 'nuxt/public/js/flowrenderer.min.js' 'nuxt/.output' 'nuxt/.netlify' && npx mkdirp 'nuxt/public/js' 'nuxt/public/css'", "build:js:nuxt": "terser -c -m -o nuxt/public/js/cc.min.js node_modules/vanilla-cookieconsent/dist/cookieconsent.umd.js nuxt/assets/js/cookieconsent-config.js && cp node_modules/@flowfuse/flow-renderer/index.min.js nuxt/public/js/flowrenderer.min.js", - "prod:postcss-nuxt": "postcss ./nuxt/assets/css/style.css -o ./nuxt/public/css/style.css --config ./postcss.config.js", - "prod:nuxt": "npm run build --workspace=nuxt", + "prod:postcss-nuxt": "postcss ./nuxt/assets/css/style.css -o ./nuxt/public/css/style.css --config ./postcss.config.cjs", + "prod:nuxt": "nuxt build nuxt", "build:nuxt": "dotenv -v NODE_ENV=production -- npm-run-all2 clean:nuxt build:js:nuxt prod:postcss-nuxt prod:nuxt", "build:nuxt:skip-images": "dotenv -v SKIP_IMAGES=true -v NODE_ENV=production -- npm-run-all2 clean:nuxt build:js:nuxt prod:postcss-nuxt prod:nuxt" }, "devDependencies": { "@flowforge/forge-ui-components": "^0.2.2", + "@nuxt/content": "^3.13.0", + "@nuxt/image": "^2.0.0", + "@nuxt/ui": "^4.8.2", + "@nuxtjs/seo": "^5.3.12", "@parcel/watcher": "2.5.6", "@resvg/resvg-js": "2.6.2", "@tailwindcss/postcss": "^4.0.0", @@ -35,6 +37,8 @@ "@takumi-rs/core": "1.8.4", "@types/markdown-it-attrs": "^4.1.3", "@types/markdown-it-footnote": "^3.0.4", + "@types/sanitize-html": "^2.16.1", + "better-sqlite3": "^12.10.0", "chokidar": "^3.6.0", "del-cli": "^5.0.0", "dotenv-cli": "^7.4.4", @@ -44,6 +48,10 @@ "netlify-plugin-cache": "^1.0.3", "nodemon": "^2.0.20", "npm-run-all2": "^7.0.1", + "nuxt": "^4.5.1", + "nuxt-link-checker": "^5.0.10", + "nuxt-og-image": "^6.7.8", + "nuxt-studio": "file:nuxt/.local-packages/nuxt-studio-1.7.0.tgz", "oxc-minify": "0.131.0", "oxc-parser": "0.134.0", "oxc-transform": "0.131.0", @@ -76,10 +84,20 @@ }, "dependencies": { "@flowfuse/flow-renderer": "^0.5.0", + "@iconify-json/heroicons": "^1.2.3", + "@iconify-json/lucide": "^1.2.123", "@netlify/blobs": "^10.7.9", "@octokit/auth-app": "^8.3.0", "algoliasearch": "^4.25.3", "js-yaml": "^4.3.2", - "markdown-it-attrs": "^4.1.6" + "markdown-it": "^14.2.0", + "markdown-it-attrs": "^4.1.6", + "mermaid": "^11.16.1", + "nuxt-llms": "^0.2.0", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", + "sanitize-html": "^2.17.7", + "satori": "^0.26.0", + "unstorage": "^1.17.5" } } diff --git a/postcss.config.js b/postcss.config.cjs similarity index 100% rename from postcss.config.js rename to postcss.config.cjs diff --git a/scripts/index-algolia.js b/scripts/index-algolia.mjs similarity index 97% rename from scripts/index-algolia.js rename to scripts/index-algolia.mjs index bdb33a5849..7e34c8d55c 100644 --- a/scripts/index-algolia.js +++ b/scripts/index-algolia.mjs @@ -1,10 +1,9 @@ #!/usr/bin/env node -const fs = require("fs/promises"); -const path = require("path"); -const algoliasearch = require("algoliasearch"); -const site = require("../nuxt/data/site.json"); -const { +import fs from "node:fs/promises"; +import path from "node:path"; +import algoliasearch from "algoliasearch"; +import { isSearchUrl, extractHeadingRecords, decodeEntities, @@ -18,7 +17,9 @@ const { extractMetaKeywords, extractDateFromJsonLd, extractArticleSection, -} = require("../lib/search-index"); +} from "../nuxt/lib/search-index.mjs"; + +const site = JSON.parse(await fs.readFile(new URL("../nuxt/data/site.json", import.meta.url), "utf8")); const BATCH_SIZE = 1000; const MAX_RECORD_BYTES = 9800; diff --git a/scripts/watch_blueprints.js b/scripts/watch_blueprints.mjs similarity index 84% rename from scripts/watch_blueprints.js rename to scripts/watch_blueprints.mjs index aa4a7c0a63..f29133ab4f 100644 --- a/scripts/watch_blueprints.js +++ b/scripts/watch_blueprints.mjs @@ -1,5 +1,5 @@ -const nodemon = require('nodemon'); -const { existsSync } = require('fs'); +import nodemon from 'nodemon'; +import { existsSync } from 'node:fs'; const pathToBlueprints = '../blueprint-library';