From 066f35681c506db22131e80d7bb6b55d4a1de8a6 Mon Sep 17 00:00:00 2001 From: Shea Leslein Date: Sun, 25 Feb 2024 23:44:04 -0600 Subject: [PATCH] Fix Remote Source Code Loading Bug (#23) * add remote source example * fix: init model when files loaded --- .../remote-src/index.tsx | 9 ++ docs/lib/remote-source.js | 81 ++++++++++++++++++ docs/pages/api/remote-files.ts | 18 ++++ docs/pages/remote-source.tsx | 83 +++++++++++++++++++ packages/code-kitchen/src/files-editor.tsx | 5 +- 5 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 docs/lib/remote-source-examples/remote-src/index.tsx create mode 100644 docs/lib/remote-source.js create mode 100644 docs/pages/api/remote-files.ts create mode 100644 docs/pages/remote-source.tsx diff --git a/docs/lib/remote-source-examples/remote-src/index.tsx b/docs/lib/remote-source-examples/remote-src/index.tsx new file mode 100644 index 0000000..3dfb98c --- /dev/null +++ b/docs/lib/remote-source-examples/remote-src/index.tsx @@ -0,0 +1,9 @@ +import React from "react"; +export default function Index() { + return ( + <> +

Hello World!

+

This source code was loaded from the server!

+ + ); +} diff --git a/docs/lib/remote-source.js b/docs/lib/remote-source.js new file mode 100644 index 0000000..0a363d7 --- /dev/null +++ b/docs/lib/remote-source.js @@ -0,0 +1,81 @@ +// Note: this file will be used in getStaticProps and must use CJS +const fsp = require("fs/promises"); +const path = require("path"); +const fs = require("fs"); + +const entryFilePattern = /^index\..*\.[tj]sx?/; +const demosBaseDir = path.resolve( + process.cwd(), + "./lib/remote-source-examples" +); +// const demosBaseDir = path.resolve(process.cwd(), './pages/demos'); + +/** + * + * @param {string} pathOrDir + * @returns {Promise} + */ +async function getRemoteEntries(pathOrDir) { + // check if dir is a directory + const stat = await fsp.stat(pathOrDir); + if (!stat.isDirectory()) { + if (entryFilePattern.test(path.basename(pathOrDir))) { + return [pathOrDir]; + } + return []; + } + // build directory structure recursively + const paths = await fsp.readdir(pathOrDir); + const results = await Promise.all( + paths.map(async (p) => { + const fullPath = path.join(pathOrDir, p); + return getRemoteEntries(fullPath); + }) + ); + return results.flat(); +} + +async function getRemoteSourceExamples() { + const paths = await getRemoteEntries(demosBaseDir); + return paths.map((p) => { + const entry = path.relative(demosBaseDir, p); + return entry.substring(0, entry.lastIndexOf("/")); + }); +} + +/** + * + * @param {string} entryDir + * @returns {Promise} + */ +async function getRemoteSourceFiles(entryDir) { + const filenames = await fsp.readdir(path.join(demosBaseDir, entryDir)); + + filenames.sort((a, b) => { + // makes sure entry is at the top + return entryFilePattern.test(a) ? -1 : 1; + }); + + const inputFiles = await Promise.all( + filenames.map(async (filename) => { + const content = await fsp.readFile( + path.join(demosBaseDir, entryDir, filename), + "utf8" + ); + // Strip off page extension + const newFilename = filename.replace(".page", ""); + return { + code: content, + filename: newFilename, + }; + }) + ); + + return inputFiles; +} + +module.exports = { + getDemos: getRemoteSourceExamples, + getDemoFiles: getRemoteSourceFiles, + getDemoEntries: getRemoteEntries, +}; diff --git a/docs/pages/api/remote-files.ts b/docs/pages/api/remote-files.ts new file mode 100644 index 0000000..e7cb89d --- /dev/null +++ b/docs/pages/api/remote-files.ts @@ -0,0 +1,18 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import { getDemoFiles } from "../../lib/remote-source"; +import { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { src } = req.query; + if (Array.isArray(src) || !src) { + return res.status(400).json({ + error: "src must be a string", + }); + } + res.json({ + files: await getDemoFiles(src), + }); +} diff --git a/docs/pages/remote-source.tsx b/docs/pages/remote-source.tsx new file mode 100644 index 0000000..1fa29ad --- /dev/null +++ b/docs/pages/remote-source.tsx @@ -0,0 +1,83 @@ +import React, { useState } from "react"; +import { useInitMonaco } from "../components/use-init-monaco"; +import { InputFile } from "code-kitchen/src/types"; +import { Playground } from "code-kitchen"; +import dependencies from "../components/dependencies"; + +export default function Page() { + const [isOpen, setIsOpen] = useState(false); + const buttonText = isOpen ? "Close Playground" : "Open Playground"; + return ( + <> + + {isOpen ? : null} + + ); +} + +function useRemoteSources(src?: string) { + const [files, setFiles] = React.useState(null); + React.useEffect(() => { + if (src) { + let cancelled = false; + fetch(`/api/remote-files?src=${encodeURIComponent(src)}`).then( + async (res) => { + if (cancelled) { + return; + } + const { files } = await res.json(); + setFiles(files); + } + ); + return () => { + cancelled = true; + }; + } + }, [src]); + return files; +} + +const customRequire = (key: string) => { + const res = (dependencies as any)[key]; + + if (res) { + return res; + } + + throw new Error("DEP: " + key + " not found"); +}; + +function RemoteSourcePlayground({ src }: { src: string }) { + useInitMonaco(); + const remoteSources = useRemoteSources(src); + const [initialFiles, setInitialFiles] = React.useState([]); + const latestInitialFiles = React.useRef([]); + + React.useEffect(() => { + const newInitialFiles = remoteSources ?? []; + setInitialFiles(newInitialFiles); + latestInitialFiles.current = newInitialFiles; + }, [remoteSources]); + + return ( + + ); +} diff --git a/packages/code-kitchen/src/files-editor.tsx b/packages/code-kitchen/src/files-editor.tsx index d3111f7..cecb025 100644 --- a/packages/code-kitchen/src/files-editor.tsx +++ b/packages/code-kitchen/src/files-editor.tsx @@ -39,7 +39,10 @@ function useModels(id: string, files: InputFile[]) { } const getFileUri = (filename: string) => monaco.Uri.file(join(id, filename)); - if (!modelsRef.current) { + if ( + !modelsRef.current || + (modelsRef.current.length === 0 && files.length > 0) + ) { const newModels = files.map((f) => { return monaco.editor.createModel( f.code,