diff --git a/README.md b/README.md index 2a77d1ca..604e53a5 100644 --- a/README.md +++ b/README.md @@ -26,21 +26,35 @@ This repository contains two main projects: #### Backend ```bash -cd Server +cd Source/SuperOffice.DocsNext dotnet restore ``` Frontend ```bash -cd ClientApp +cd Source/SuperOffice.DocsNext/ClientApp npm install ``` -### 3. Run Development Servers +### 3. Clone external repos + +You can either +1. Manually clone or copy files of `SuperOfficeDocs/superoffice-docs` and `SuperOfficeDocs/contribution` repos into `ClientApp/src/external-content` + +2. Use following script to clone or update the required external GitHub repositories into the `ClientApp/src/external-content/` directory. +If a repository folder already exists, script will fetch and reset it to the latest commit on the `main` branch. + + ```bash + cd Source/SuperOffice.DocsNext/ClientApp/build + node setup-external-repos.js + ``` + + +### 4. Run Development Servers #### Backend (with API and proxy to frontend) -From docs-next/Server: +From docs-next/Source/SuperOffice.DocsNext: ```bash dotnet run ``` @@ -49,7 +63,7 @@ By default API runs at: http://localhost:5215/api. Any non-API request is proxie #### Frontend (Astro dev server) -From docs-next/ClientApp: +From docs-next/Source/SuperOffice.DocsNext/ClientApp: ```bash npm run dev @@ -61,7 +75,15 @@ By default Frontend dev server runs at: http://localhost:4321. The backend proxi Any other path → served by Astro dev server. -### 4. Run Production Build +##### Reduce content during development (dev:partial) + +To manage the content during development, npm command to run dev server with reduced content was introduced. This is useful when you only need to run the development server without the content from superoffice-docs. It uses a pre-defined enviornment variable (PARTIAL_BUILD) to disable content collections from rendering. + +```bash +npm run dev:partial +``` + +### 5. Run Production Build 1. Build Backend (includes frontend) @@ -81,6 +103,14 @@ dotnet docs-next.dll API: https://localhost:5001/api/... Frontend: served from wwwroot +#### Partial frontend build (build:partial) + +To reduce build time when testing a build, npm command to do partial builds was introduced. This is useful when you only need to build the frontend without the content from superoffice-docs. It uses a pre-defined enviornment variable (PARTIAL_BUILD) to disable content collections from building. + +```bash +npm run build:partial +``` + ### Notes In development, run both servers: diff --git a/Source/SuperOffice.DocsNext/ClientApp/astro.config.mjs b/Source/SuperOffice.DocsNext/ClientApp/astro.config.mjs index 19d141bf..cc0535c8 100644 --- a/Source/SuperOffice.DocsNext/ClientApp/astro.config.mjs +++ b/Source/SuperOffice.DocsNext/ClientApp/astro.config.mjs @@ -4,14 +4,11 @@ import icon from "astro-icon"; import remarkDirective from "remark-directive"; import codeImport from "remark-code-import"; import mdx from "@astrojs/mdx"; -// import preact from "@astrojs/preact"; import robots from "astro-robots"; import sitemap from "@astrojs/sitemap"; import pagefind from "astro-pagefind"; import { rehypeHeadingIds } from "@astrojs/markdown-remark"; -//import rehypeSlug from 'rehype-slug'; import rehypeAutolinkHeadings from "rehype-autolink-headings"; -// import rehypeSanitize from "rehype-sanitize"; import remarkIncludeDirective from "./src/plugins/AddIncludesToMarkdown.js"; import remarkRestyleDirective from "./src/plugins/RestyleDirectives.js"; import react from "@astrojs/react"; @@ -19,13 +16,10 @@ import yaml from '@rollup/plugin-yaml'; import redirectFrom from "astro-redirect-from"; import { getRedirectFromSlug } from './src/utils/slugUtils.ts'; -const apiOnly = process.env.API_ONLY === 'true'; export default defineConfig({ - // Conditionally exclude static landing page pages: [ - 'src/pages/**/*', - ...(apiOnly ? ['!src/pages/contribute/index.astro'] : []), + 'src/pages/**/*' ], markdown: { @@ -39,7 +33,6 @@ export default defineConfig({ }, ], ], - // rehypeSanitize, rehypeSlug shikiConfig: { theme: "houston", wrap: true, @@ -96,13 +89,14 @@ export default defineConfig({ }, }), mdx(), - // pagefind(), + pagefind(), react(), // redirectFrom({ // contentDir: './external-content', // getSlug: getRedirectFromSlug, // Function to get the slug for redirect_from // }), - robots(), sitemap(), + robots(), + sitemap(), ], image: { diff --git a/Source/SuperOffice.DocsNext/ClientApp/build/detect-duplicate-frontmatter.js b/Source/SuperOffice.DocsNext/ClientApp/build/detect-duplicate-frontmatter.js index b3067358..a5f75237 100644 --- a/Source/SuperOffice.DocsNext/ClientApp/build/detect-duplicate-frontmatter.js +++ b/Source/SuperOffice.DocsNext/ClientApp/build/detect-duplicate-frontmatter.js @@ -1,5 +1,18 @@ -// detect-duplicate-frontmatter.js -// Usage: node detect-duplicate-frontmatter.js +/** + * Duplicate Frontmatter Detection Tool + * + * This script analyzes Markdown files for duplicate frontmatter properties + * and generates a report of any duplicates found. + * + * Usage: + * go to ClientApp/build directory + * node detect-duplicate-frontmatter.js + * + * Prerequisites: + * - Node.js installed + * - Run from the script's directory + * + */ import fs from "fs"; import path from "path"; @@ -9,7 +22,7 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const contentDir = path.resolve("external-content/superoffice-docs/docs"); +const contentDir = path.resolve("../external-content/superoffice-docs"); const outputFile = path.resolve(__dirname, "duplicate-frontmatter.txt"); const extensions = new Set([".md"]); // add ".mdx" if needed @@ -60,7 +73,6 @@ function findTopLevelDuplicateKeys(yamlText) { const files = fs.existsSync(contentDir) ? walk(contentDir) : []; /** @type {{file:string, duplicates:string[]}[]} */ const problemFiles = []; - for (const file of files) { const raw = fs.readFileSync(file, "utf8"); const fm = extractFrontmatter(raw); diff --git a/Source/SuperOffice.DocsNext/ClientApp/build/local-build-script.mjs b/Source/SuperOffice.DocsNext/ClientApp/build/local-build-script.mjs index af2a9b99..1be17dbd 100644 --- a/Source/SuperOffice.DocsNext/ClientApp/build/local-build-script.mjs +++ b/Source/SuperOffice.DocsNext/ClientApp/build/local-build-script.mjs @@ -1,3 +1,35 @@ +/** + * Local build script for split builds + * + * This script performs a two-phase build process and merges the results: + * 1. Builds with API_ONLY=true + * 2. Builds with API_ONLY=false + * 3. Merges both builds + * 4. Indexes the result with Pagefind + * + * @description + * To use this script for split builds: + * 1. Set up your content collections to handle API_ONLY environment variable + * 2. Configure other files to respond to API_ONLY flag appropriately + * 3. Run this script instead of regular astro build + * + * @example + * // In your content collections: + * const items = API_ONLY === 'true' + * ? apiOnlyContent + * : fullContent; + * + * @note + * - Cleans up temporary build directories (.distA and .distB) + * - Final output will be in dist/ directory + * - Automatically runs Pagefind indexing on final build + * + * @warning + * Currently not actively used in the codebase. + * Ensure proper setup before running split builds. + */ + + import { execSync } from "child_process"; import { cpSync, rmSync, existsSync } from "fs"; import path from "path"; diff --git a/Source/SuperOffice.DocsNext/ClientApp/build/setup-external-repos.js b/Source/SuperOffice.DocsNext/ClientApp/build/setup-external-repos.js new file mode 100644 index 00000000..f04d939b --- /dev/null +++ b/Source/SuperOffice.DocsNext/ClientApp/build/setup-external-repos.js @@ -0,0 +1,74 @@ +/** + * Setup External Repositories + * + * This script clones or updates the required external GitHub repositories + * into the `src/external-content/` directory. If a repository folder + * already exists, it will fetch and reset it to the latest commit on the + * `main` branch. + * + * Usage: + * # From ClientApp/build directory + * node setup-external-repos.js + * + * Notes: + * - Requires Git to be installed and available in PATH. + * - Local changes inside the external repos will be overwritten when updating. + */ + + + + +import { exec } from "node:child_process"; +import path from "node:path"; +import fs from "node:fs"; + +const baseDir = path.resolve(process.cwd(), "..", "external-content"); + +const repos = [ + { + url: "https://github.com/SuperOfficeDocs/contribution.git", + dest: path.join(baseDir, "contribution"), + }, + { + url: "https://github.com/SuperOfficeDocs/superoffice-docs.git", + dest: path.join(baseDir, "superoffice-docs"), + }, +]; + +function runCommand(command, cwd) { + return new Promise((resolve, reject) => { + exec(command, { cwd }, (error, stdout, stderr) => { + if (error) { + console.error(`Command failed: ${command}\n${stderr}`); + return reject(error); + } + resolve(stdout.trim()); + }); + }); +} + +async function cloneOrUpdate({ url, dest }) { + if (fs.existsSync(dest)) { + console.log(`Repo already exists at ${dest}. Pulling latest changes...`); + await runCommand("git fetch --all", dest); + await runCommand("git reset --hard origin/main", dest); + console.log(`Updated ${url}`); + } else { + console.log(`Cloning ${url} into ${dest}...`); + await runCommand(`git clone ${url} "${dest}"`, process.cwd()); + console.log(`Successfully cloned ${url}`); + } +} + +async function main() { + for (const repo of repos) { + try { + await cloneOrUpdate(repo); + } catch { + process.exitCode = 1; + break; + } + } +} + +main(); diff --git a/Source/SuperOffice.DocsNext/ClientApp/package.json b/Source/SuperOffice.DocsNext/ClientApp/package.json index 124cb240..23d59f14 100644 --- a/Source/SuperOffice.DocsNext/ClientApp/package.json +++ b/Source/SuperOffice.DocsNext/ClientApp/package.json @@ -4,10 +4,10 @@ "version": "0.0.1", "scripts": { "dev": "astro dev", + "dev:partial": "powershell -Command \"$env:PARTIAL_BUILD='true'; astro dev\"", "start": "astro dev", - "build": "cross-env NODE_OPTIONS=\"--max-old-space-size=12288\" astro check && astro build", - "build:default": "astro check && astro build", - "build:local": "node build/local-build-script.mjs", + "build": "astro check && astro build", + "build:partial": "powershell -Command \"$env:PARTIAL_BUILD='true'; astro check; astro build\"", "preview": "astro preview", "astro": "astro", "test:e2e": "npm run test --workspace=e2e-tests" diff --git a/Source/SuperOffice.DocsNext/ClientApp/src/content.config.ts b/Source/SuperOffice.DocsNext/ClientApp/src/content.config.ts index 25b0631f..d057a0ee 100644 --- a/Source/SuperOffice.DocsNext/ClientApp/src/content.config.ts +++ b/Source/SuperOffice.DocsNext/ClientApp/src/content.config.ts @@ -3,15 +3,15 @@ import { defineCollection } from "astro:content"; import { glob } from "astro/loaders"; import { DocsSchema, SimplifiedYamlSchema, TocYamlSchema, YamlManagedReferenceSchema } from "~/content.schema" -// apiOnly variable is used in the split build to isolate docs/en/api folder content -const apiOnly = process.env.API_ONLY === 'true'; +// partialBuild variable is used to partially build the content for development purposes. +const partialBuild = process.env.PARTIAL_BUILD === 'true'; const DOCS_BASE = "external-content/superoffice-docs/docs"; const API_BASE = `${DOCS_BASE}/en/api`; const enDocs = defineCollection({ loader: glob({ - pattern: apiOnly ? [] : [ + pattern: partialBuild ? [] : [ "**/*.md", "!index.md", "!**/includes/**", @@ -25,7 +25,7 @@ const enDocs = defineCollection({ const apiDocs = defineCollection({ loader: glob({ - pattern: apiOnly ? [] : [ + pattern: partialBuild ? [] : [ "**/*.md", "!**/includes/**", "!tutorials/minimal-csharp-app", //Temporary excluded due to corrupted images @@ -45,10 +45,10 @@ const apiDocs = defineCollection({ const CRMScript = defineCollection({ loader: glob({ - pattern: apiOnly ? [ + pattern: partialBuild ? [] : [ "**/!(*toc).yml", "!**/includes/**", - ] : [], + ], base: `${DOCS_BASE}/en/automation/crmscript/reference`, }), schema: YamlManagedReferenceSchema, @@ -56,7 +56,7 @@ const CRMScript = defineCollection({ const NSScriptingRef = defineCollection({ loader: glob({ - pattern: apiOnly ? [] : [ + pattern: partialBuild ? [] : [ "**/*.md", "!**/includes/**",], base: `${DOCS_BASE}/en/automation/netserver-scripting/reference`, @@ -64,47 +64,13 @@ const NSScriptingRef = defineCollection({ schema: DocsSchema, }); -// const WebAPI = defineCollection({ -// loader: glob({ -// pattern: false ? ["**/!(*toc).yml"] : [], -// base: `${API_BASE}/reference/webapi` -// }), -// }); - -// const Web = defineCollection({ -// loader: glob({ -// pattern: apiOnly ? [ -// "*.yml", -// ] : [], -// base: `${API_BASE}/reference/web` -// }), -// }); - -// const NetserverCore = defineCollection({ -// loader: glob({ -// pattern: apiOnly ? [ -// "*.yml", -// ] : [], -// base: `${API_BASE}/reference/netserver/core` -// }), -// }); - -// const NetserverServices = defineCollection({ -// loader: glob({ -// pattern: apiOnly ? [ -// "*.yml", -// ] : [], -// base: `${API_BASE}/reference/netserver/services` -// }), -// }); - /** * TRANSLATIONS */ const daDocs = defineCollection({ loader: glob({ - pattern: apiOnly ? [] : [ + pattern: partialBuild ? [] : [ "**/*.md", "!**/includes/**", ], @@ -115,7 +81,7 @@ const daDocs = defineCollection({ const deDocs = defineCollection({ loader: glob({ - pattern: apiOnly ? [] : [ + pattern: partialBuild ? [] : [ "**/*.md", "!**/includes/**", ], @@ -126,7 +92,7 @@ const deDocs = defineCollection({ const nlDocs = defineCollection({ loader: glob({ - pattern: apiOnly ? [] : [ + pattern: partialBuild ? [] : [ "**/*.md", "!**/includes/**", ], @@ -137,7 +103,7 @@ const nlDocs = defineCollection({ const noDocs = defineCollection({ loader: glob({ - pattern: apiOnly ? [] : [ + pattern: partialBuild ? [] : [ "**/*.md", "!**/includes/**", ], @@ -148,7 +114,7 @@ const noDocs = defineCollection({ const svDocs = defineCollection({ loader: glob({ - pattern: apiOnly ? [] : [ + pattern: partialBuild ? [] : [ "**/*.md", "!**/includes/**", ], @@ -163,7 +129,7 @@ const svDocs = defineCollection({ const contribution = defineCollection({ loader: glob({ - pattern: apiOnly ? [] : [ + pattern: partialBuild ? [] : [ "**/*.md", "!**/includes/**", "!CODE_OF_CONDUCT.md", @@ -175,7 +141,7 @@ const contribution = defineCollection({ const releaseNotes = defineCollection({ loader: glob({ - pattern: apiOnly ? [] : [ + pattern: partialBuild ? [] : [ "**/*.md", "!**/includes/**", ], @@ -220,10 +186,6 @@ export const collections = { "api-docs": apiDocs, "crmscript": CRMScript, "nsscripting": NSScriptingRef, - // "netserver-core": NetserverCore, - // "netserver-services": NetserverServices, - // webapi: WebAPI, - // web: Web, contribute: contribution, "release-notes": releaseNotes, cats: landingPages, diff --git a/Source/SuperOffice.DocsNext/ClientApp/src/pages/en/api/reference/[...mdslug].astro b/Source/SuperOffice.DocsNext/ClientApp/src/pages/en/api/reference/[...mdslug].astro index f553f1c1..35b99dbc 100644 --- a/Source/SuperOffice.DocsNext/ClientApp/src/pages/en/api/reference/[...mdslug].astro +++ b/Source/SuperOffice.DocsNext/ClientApp/src/pages/en/api/reference/[...mdslug].astro @@ -4,14 +4,14 @@ import { glob } from "glob"; import path from "path"; import matter from "gray-matter"; import type { TocData } from "~/types/TableOfContentTypes"; -import { getTocByPath } from "@utils/tocUtils"; +import { loadAPITocData } from "@utils/tocUtils"; import ContentLayout from "@layouts/ContentLayout.astro"; import { renderMarkdownWithHeadingIds } from "@utils/contentUtils"; export async function getStaticPaths() { - const apiOnly = process.env.API_ONLY === 'true'; + const partialBuild = process.env.PARTIAL_BUILD === 'true'; - if (!apiOnly) { + if (partialBuild) { return []; } @@ -47,22 +47,6 @@ export async function getStaticPaths() { }; } - // Function to create lazy loader for TOC data - function loadTocData(apiPath: string) { - return async () => { - try { - const tocData = await getTocByPath(apiPath); - return tocData; - } catch (error) { - console.warn(`Failed to load TOC data for path: ${apiPath}`, error); - // Return a default/empty TOC structure instead of failing - return { - items: [], - } as TocData; - } - }; - } - try { // Temporary configured to exclude "soap" until github pages limitation is solved const pattern = [ @@ -90,17 +74,17 @@ export async function getStaticPaths() { // Generate slug from the relative path, removing base and extension // Remove "superoffice-docs/docs/en/api/reference/" and file extension to get clean slug - const cleanPath = normalizedPath + const slug = normalizedPath .replace("superoffice-docs/docs/en/api/reference", "") - .replace(/\.(md|mdx)$/, ""); - const slug = cleanPath; + .replace(/\.(md|mdx)$/, "") + .replace(/\/index$/, ""); const apiType = slug?.split("/")[1]; const tocCollectionPath = `superoffice-docs/docs/en/api/reference/${apiType}`; // Cache the TOC loader function, not the data if (!tocLoaderCache.has(apiType)) { - tocLoaderCache.set(apiType, loadTocData(tocCollectionPath)); + tocLoaderCache.set(apiType, loadAPITocData(tocCollectionPath)); } return { diff --git a/Source/SuperOffice.DocsNext/ClientApp/src/pages/en/api/reference/[...yamlslug].astro b/Source/SuperOffice.DocsNext/ClientApp/src/pages/en/api/reference/[...yamlslug].astro index c3579c29..915049f2 100644 --- a/Source/SuperOffice.DocsNext/ClientApp/src/pages/en/api/reference/[...yamlslug].astro +++ b/Source/SuperOffice.DocsNext/ClientApp/src/pages/en/api/reference/[...yamlslug].astro @@ -3,20 +3,17 @@ import { readFile } from "fs/promises"; import { glob } from "glob"; import path from "path"; import yaml from "js-yaml"; -import { getTocByPath } from "@utils/tocUtils"; +import { loadAPITocData } from "@utils/tocUtils"; import { getYamlReferenceSlug } from "@utils/slugUtils"; import YamlLayoutPage from "@layouts/YamlLayoutPage.astro"; import type { TocData } from "~/types/TableOfContentTypes"; export async function getStaticPaths() { + const partialBuild = process.env.PARTIAL_BUILD === 'true'; - const apiOnly = process.env.API_ONLY === 'true'; - - if (!apiOnly) { + if (partialBuild) { return []; } - - const baseContentPath = "external-content"; const apiCollections = [ @@ -40,18 +37,6 @@ export async function getStaticPaths() { }; } - // Lazy loader for TOC - function loadTocData(apiPath: string) { - return async () => { - try { - return await getTocByPath(apiPath); - } catch (error) { - console.warn(`Failed to load TOC for ${apiPath}`, error); - return { items: [] } as TocData; - } - }; - } - function getApiType(path:string):string{ if(path.startsWith("netserver")){ return path @@ -86,14 +71,14 @@ export async function getStaticPaths() { ); const apiType = getApiType(match ? match[1] : "unknown"); - const cleanPath = normalizedPath + const slug = normalizedPath .replace("superoffice-docs/docs/en/api/reference", "") .replace(/\.(yml|yaml)$/, ""); - const slug = cleanPath; + const tocCollectionPath = `superoffice-docs/docs/en/api/reference/${apiType}`; if (!tocLoaderCache.has(apiType)) { - tocLoaderCache.set(apiType, loadTocData(tocCollectionPath)); + tocLoaderCache.set(apiType, loadAPITocData(tocCollectionPath)); } return { diff --git a/Source/SuperOffice.DocsNext/ClientApp/src/utils/tocUtils.ts b/Source/SuperOffice.DocsNext/ClientApp/src/utils/tocUtils.ts index e9c6daae..27a2dcaf 100644 --- a/Source/SuperOffice.DocsNext/ClientApp/src/utils/tocUtils.ts +++ b/Source/SuperOffice.DocsNext/ClientApp/src/utils/tocUtils.ts @@ -117,3 +117,24 @@ export async function getTocByPath(path: string) { ); return getTableOfContentsFromCollection(tocEntries, path); } + + +/** + * Creates an async function that loads table of contents (TOC) data from a specified API path. + * + * @param apiPath - The API path to fetch TOC data from + * @returns An async function that when called: + * - Returns the TOC data if successful + * - Returns an empty TOC data structure ({items: []}) if the fetch fails + * @throws Catches and logs any errors during TOC data fetching + */ + export function loadAPITocData(apiPath: string) { + return async () => { + try { + return await getTocByPath(apiPath); + } catch (error) { + console.warn(`Failed to load TOC for ${apiPath}`, error); + return { items: [] } as TocData; + } + }; + } \ No newline at end of file diff --git a/Source/SuperOffice.DocsNext/SuperOffice.DocsNext.csproj b/Source/SuperOffice.DocsNext/SuperOffice.DocsNext.csproj index 11597917..7e0a6f1a 100644 --- a/Source/SuperOffice.DocsNext/SuperOffice.DocsNext.csproj +++ b/Source/SuperOffice.DocsNext/SuperOffice.DocsNext.csproj @@ -25,11 +25,11 @@ - + - +