diff --git a/vuu-ui/tools/showcase-cli/README.md b/vuu-ui/tools/showcase-cli/README.md new file mode 100644 index 000000000..84cce5b80 --- /dev/null +++ b/vuu-ui/tools/showcase-cli/README.md @@ -0,0 +1,27 @@ +# Showcase CLI + +## startup sequence + +#### cli.mjs + +- load index.html from templates +- get config file from input args, default to 'showcase.config.json' +- check config file exists +- check ./.showcase folder exists + if not - create ./.showcase - copy index.html to .showcase + if ../dist folder exists (this will be part of published package) - copy build files from ../dist to .showcase +- read config json from config file +- validate that directory exists at `config.exhibits` +- build exhibit structure from files at `config.exhibits` +- write the packageTree structure out to .showcase +- call (main.ts).start(config) + +## Next steps + +- once package tree is created, creat import maps +- inject importmaps into index.html +- update Tree to allow runtime node expansion +- update showcase-standalone + +- integrate into vuu cli, using stricli. Parameter parsing will happen there +- use jumpgen to monitor file system for new/edited exhibits in dev mode diff --git a/vuu-ui/tools/showcase-cli/cli.js b/vuu-ui/tools/showcase-cli/cli.js index 2ed041e94..556ff4208 100755 --- a/vuu-ui/tools/showcase-cli/cli.js +++ b/vuu-ui/tools/showcase-cli/cli.js @@ -10,39 +10,50 @@ import { } from "./cli/cli-utils.ts"; import indexHtml from "./templates/index.html.ts"; import { fileURLToPath } from "url"; +import start from "./cli/main.ts"; +import { buildPackageTree } from "./cli/buildPackageTree"; /** Parse the command line */ var args = process.argv.slice(2); -// Validate input -if (args.length !== 1) { - console.log("Warning: Requires 1 argument"); - console.log("node config-path"); - process.exit(); -} +let configFilePath = "./showcase.config.json"; -const configPath = args[0]; - -// const dirsrc = path.dirname(configPath); -if (!fs.existsSync(configPath)) { - console.log("Error: Config file doesn't exist. Given: ", configPath); - process.exit(); +// Validate input +if (args.length === 0) { + if (!fs.existsSync(configFilePath)) { + console.log("Warning: Requires 1 argument, path to config file. "); + process.exit(); + } else { + console.log("using config file at './showcase.config.json'"); + } +} else { + if (fs.existsSync(args[0])) { + configFilePath = args[0]; + } else { + console.log( + `Warning: first argument ${args[0]} should be path to config file, file not found`, + ); + process.exit(); + } } -const templateDir = path.resolve(fileURLToPath(import.meta.url), "../dist"); +const distFolder = path.resolve(fileURLToPath(import.meta.url), "../dist"); if (!fs.existsSync(".showcase")) { createFolder(".showcase"); + // DOn't do this until we create importmaps await writeFile(indexHtml, "./.showcase/index.html"); } else { console.log(".showcase folder present and correct"); } -if (fs.existsSync(templateDir)) { - copyFiles(templateDir, "./.showcase"); +// TODO check whether dist files already present in .showcase +if (fs.existsSync(distFolder)) { + copyFiles(distFolder, "./.showcase"); } -const config = readJson(configPath); +const config = readJson(configFilePath); + //TODO use type validator to check config file const { exhibits } = config; if (!fs.existsSync(exhibits)) { @@ -50,6 +61,12 @@ if (!fs.existsSync(exhibits)) { process.exit(); } -import("./cli/main.ts").then(({ default: start }) => { - start(config); -}); +const stories = buildPackageTree(exhibits); +await writeFile( + `export default ${JSON.stringify(stories, null, 2)};`, + "./.showcase/exhibits.js", +); + +console.log(JSON.stringify(stories, null, 2)); + +start(config); diff --git a/vuu-ui/tools/showcase-cli/cli/buildPackageTree.ts b/vuu-ui/tools/showcase-cli/cli/buildPackageTree.ts index b03cb8442..03a3d6cf8 100644 --- a/vuu-ui/tools/showcase-cli/cli/buildPackageTree.ts +++ b/vuu-ui/tools/showcase-cli/cli/buildPackageTree.ts @@ -13,7 +13,7 @@ export const buildPackageTree = (dir: string, tree = {}) => { } } else if (fileName.match(/(examples.tsx|.mdx)$/)) { const [storyName] = fileName.split("."); - tree[storyName] = `${dir}${fileName}`; + tree[storyName] = fileName; } }); return tree; diff --git a/vuu-ui/tools/showcase-cli/cli/main.ts b/vuu-ui/tools/showcase-cli/cli/main.ts index bfb2fa959..0429e2089 100644 --- a/vuu-ui/tools/showcase-cli/cli/main.ts +++ b/vuu-ui/tools/showcase-cli/cli/main.ts @@ -1,6 +1,5 @@ import { createServer } from "vite"; import { HttpServerConfig, startHTTPServer } from "./http-server"; -import { buildPackageTree } from "./buildPackageTree"; export type ShowcaseConfig = { exhibits: string; @@ -9,11 +8,6 @@ export type ShowcaseConfig = { }; export default async (config: ShowcaseConfig) => { - const start = performance.now(); - const stories = buildPackageTree(config.exhibits); - const end = performance.now(); - console.log(`building exhibits menu took ${end - start}ms`); - console.log(JSON.stringify(stories, null, 2)); // fs.writeFile(OUT, JSON.stringify(stories, null, 2), (err) => { // if (err) { // console.log(err); diff --git a/vuu-ui/tools/showcase-cli/src/App.tsx b/vuu-ui/tools/showcase-cli/src/App.tsx index 79fe906bf..4c609bc26 100644 --- a/vuu-ui/tools/showcase-cli/src/App.tsx +++ b/vuu-ui/tools/showcase-cli/src/App.tsx @@ -1,5 +1,6 @@ +import React from "react"; import { Flexbox } from "@finos/vuu-layout"; -import { Tree, TreeSourceNode } from "@finos/vuu-ui-controls"; +import { Tree } from "@finos/vuu-ui-controls"; import { Density, ThemeMode } from "@finos/vuu-utils"; import { Button, @@ -9,42 +10,17 @@ import { ToggleButtonGroup, } from "@salt-ds/core"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useLocation } from "react-router-dom"; import { IFrame } from "./components"; -import { byDisplaySequence, ExamplesModule, loadTheme } from "./showcase-utils"; - +import { loadTheme } from "./showcase-utils"; +import type { ExhibitsJson } from "./exhibit-utils"; import { ThemeSwitch } from "@finos/vuu-shell"; import "./App.css"; +import { useShowcaseApp } from "./useShowcaseApp"; -const sourceFromImports = ( - stories: ExamplesModule, - prefix = "", - icon = "folder", -): TreeSourceNode[] => - Object.entries(stories) - .filter(([path]) => path !== "default") - .sort(byDisplaySequence) - .map(([label, stories]) => { - const id = `${prefix}${label}`; - // TODO how can we know when a potential docs node has docs - // console.log(`id=${id}`); - if (typeof stories === "function") { - return { - id, - icon: "rings", - label, - }; - } - return { - id, - icon, - label, - childNodes: sourceFromImports(stories, `${id}/`, "box"), - }; - }); export interface AppProps { - stories: ExamplesModule; + exhibits: ExhibitsJson; } type ThemeDescriptor = { label?: string; id: string }; @@ -70,10 +46,11 @@ const availableDensity: DensityDescriptor[] = [ { id: "touch", label: "Touch" }, ]; -export const App = ({ stories }: AppProps) => { - const navigate = useNavigate(); +export const App = ({ exhibits }: AppProps) => { const [themeReady, setThemeReady] = useState(false); + const { onSelectionChange, source } = useShowcaseApp({ exhibits }); + useEffect(() => { loadTheme("vuu-theme").then(() => { setThemeReady(true); @@ -81,9 +58,7 @@ export const App = ({ stories }: AppProps) => { }, []); // // TODO cache source in localStorage - const source = useMemo(() => sourceFromImports(stories), [stories]); const { pathname } = useLocation(); - const handleChange = ([selected]: TreeSourceNode[]) => navigate(selected.id); const [themeIndex, setThemeIndex] = useState(2); const [themeModeIndex, setThemeModeIndex] = useState(0); const [densityIndex, setDensityIndex] = useState(0); @@ -134,7 +109,7 @@ export const App = ({ stories }: AppProps) => { style={{ flex: "0 0 200px" }} data-resizeable selected={[pathname.slice(1)]} - onSelectionChange={handleChange} + onSelectionChange={onSelectionChange} revealSelected source={source} /> diff --git a/vuu-ui/tools/showcase-cli/src/Showcase.tsx b/vuu-ui/tools/showcase-cli/src/Showcase.tsx index 04a687274..9232ea89b 100644 --- a/vuu-ui/tools/showcase-cli/src/Showcase.tsx +++ b/vuu-ui/tools/showcase-cli/src/Showcase.tsx @@ -1,10 +1,12 @@ -import "./Showcase.css"; - +import React from "react"; import { BrowserRouter, Route, Routes } from "react-router-dom"; -import { ExamplesModule } from "./showcase-utils"; import { App } from "./App"; +import { ExamplesModule } from "./showcase-utils"; + +import "./Showcase.css"; +import { ExhibitsJson } from "./exhibit-utils"; -const createRoutes = (examples: ExamplesModule, prefix = ""): JSX.Element[] => +const createRoutes = (examples: ExhibitsJson, prefix = ""): JSX.Element[] => Object.entries(examples) .filter(([path]) => path !== "default") .reduce((routes, [label, Value]) => { @@ -16,11 +18,11 @@ const createRoutes = (examples: ExamplesModule, prefix = ""): JSX.Element[] => : routes.concat(); }, []); -export const Showcase = ({ exhibits }: { exhibits: ExamplesModule }) => { +export const Showcase = ({ exhibits }: { exhibits: ExhibitsJson }) => { return ( - }> + }> {createRoutes(exhibits)} diff --git a/vuu-ui/tools/showcase-cli/src/exhibit-utils.ts b/vuu-ui/tools/showcase-cli/src/exhibit-utils.ts new file mode 100644 index 000000000..d55320178 --- /dev/null +++ b/vuu-ui/tools/showcase-cli/src/exhibit-utils.ts @@ -0,0 +1,3 @@ +export interface ExhibitsJson { + [key: string]: string | ExhibitsJson; +} diff --git a/vuu-ui/tools/showcase-cli/src/index-main.tsx b/vuu-ui/tools/showcase-cli/src/index-main.tsx index 6d17ecf50..052634561 100644 --- a/vuu-ui/tools/showcase-cli/src/index-main.tsx +++ b/vuu-ui/tools/showcase-cli/src/index-main.tsx @@ -1,8 +1,11 @@ import ReactDOM from "react-dom"; import React from "react"; import { Showcase } from "./Showcase"; +import { ExhibitsJson } from "./exhibit-utils"; -const root = document.getElementById("root") as HTMLDivElement; -// The full Showcase shell loads all examples in order to render the Navigation Tree. This can -// be a bit slow in dev mode. -ReactDOM.render(, root); +export default (exhibits: ExhibitsJson) => { + const root = document.getElementById("root") as HTMLDivElement; + // The full Showcase shell loads all examples in order to render the Navigation Tree. This can + // be a bit slow in dev mode. + ReactDOM.render(, root); +}; diff --git a/vuu-ui/tools/showcase-cli/src/index-standalone.tsx b/vuu-ui/tools/showcase-cli/src/index-standalone.tsx index 2d0ede983..fd8fc6258 100644 --- a/vuu-ui/tools/showcase-cli/src/index-standalone.tsx +++ b/vuu-ui/tools/showcase-cli/src/index-standalone.tsx @@ -1,6 +1,7 @@ import { ShowcaseStandalone } from "@finos/vuu-showcase"; import ReactDOM from "react-dom"; -const root = document.getElementById("root") as HTMLDivElement; - -ReactDOM.render(, root); +export default (exhibits: unknown) => { + const root = document.getElementById("root") as HTMLDivElement; + ReactDOM.render(, root); +}; diff --git a/vuu-ui/tools/showcase-cli/src/main.ts b/vuu-ui/tools/showcase-cli/src/main.ts index 6fd3ab731..c18ba96db 100644 --- a/vuu-ui/tools/showcase-cli/src/main.ts +++ b/vuu-ui/tools/showcase-cli/src/main.ts @@ -1,6 +1,15 @@ import { hasUrlParameter } from "@finos/vuu-utils"; -if (hasUrlParameter("standalone")) { - import("./index-standalone"); -} else { - import("./index-main"); -} +import { ExhibitsJson } from "./exhibit-utils"; + +export default async (exhibits: ExhibitsJson) => { + console.log("Showcase start", { + exhibits, + }); + if (hasUrlParameter("standalone")) { + const { default: start } = await import("./index-standalone"); + start(exhibits); + } else { + const { default: start } = await import("./index-main"); + start(exhibits); + } +}; diff --git a/vuu-ui/tools/showcase-cli/src/useShowcaseApp.ts b/vuu-ui/tools/showcase-cli/src/useShowcaseApp.ts new file mode 100644 index 000000000..f7a82f860 --- /dev/null +++ b/vuu-ui/tools/showcase-cli/src/useShowcaseApp.ts @@ -0,0 +1,69 @@ +import { TreeSourceNode } from "@finos/vuu-ui-controls"; +import { useCallback, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import { AppProps } from "./App"; +import { ExhibitsJson } from "./exhibit-utils"; + +const sourceFromImports = ( + exhibits: ExhibitsJson, + prefix = "", + icon = "folder", +): TreeSourceNode[] => + Object.entries(exhibits).map(([label, exhibits]) => { + const id = `${prefix}${label}`; + if (typeof exhibits === "string") { + return { + id, + icon: "rings", + label, + }; + } + return { + id, + icon, + label, + childNodes: sourceFromImports(exhibits, `${id}/`, "box"), + }; + }); + +const getTargetExhibit = (exhibits: ExhibitsJson, path: string) => { + const steps = path.split("/"); + const root = steps.slice(0, -1).join("/"); + + let node: string | ExhibitsJson = exhibits; + let pathRoot: string[] = []; + while (steps.length) { + const step = steps.shift() as string; + node = node[step]; + } + if (typeof node === "string") { + return `${root}/${node}`; + } else { + throw Error(`unexpected leaf node ${JSON.stringify(node)}`); + } +}; + +export const useShowcaseApp = ({ exhibits }: AppProps) => { + const navigate = useNavigate(); + + const source = useMemo(() => sourceFromImports(exhibits), [exhibits]); + + const handleChange = async ([selected]: TreeSourceNode[]) => { + console.log(JSON.stringify(selected, null, 2)); + + const sourceTarget = getTargetExhibit(exhibits, selected.id); + if (sourceTarget?.endsWith(".tsx")) { + const module = await import( + /* @vite-ignore */ + `exhibits:src/examples/${sourceTarget}` + ); + console.log(module); + } + // navigate(selected.id); + }; + + return { + onSelectionChange: handleChange, + source, + }; +}; diff --git a/vuu-ui/tools/showcase-cli/templates/index.html.ts b/vuu-ui/tools/showcase-cli/templates/index.html.ts index 80e9f0660..b03b28fcb 100644 --- a/vuu-ui/tools/showcase-cli/templates/index.html.ts +++ b/vuu-ui/tools/showcase-cli/templates/index.html.ts @@ -2,12 +2,19 @@ export default ` Vuu Showcase - + - +