diff --git a/package-lock.json b/package-lock.json index 793b9677..7e2f5a34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "shinylive", - "version": "0.2.8", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "shinylive", - "version": "0.2.8", + "version": "0.3.0", "license": "MIT", "devDependencies": { "@codemirror/autocomplete": "^6.4.2", @@ -61,6 +61,7 @@ "prettier-plugin-organize-imports": "^3.2.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", "tsx": "^4.7.0", "typescript": "^5.3.3", "vscode-languageserver-protocol": "^3.17.5", @@ -4982,6 +4983,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "dev": true, + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -8439,6 +8449,22 @@ "react": "^18.2.0" } }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "dev": true, + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, "node_modules/react-icons": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.12.0.tgz", diff --git a/package.json b/package.json index aab249ba..5935706f 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "prettier-plugin-organize-imports": "^3.2.4", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", "tsx": "^4.7.0", "typescript": "^5.3.3", "vscode-languageserver-protocol": "^3.17.5", @@ -142,6 +143,5 @@ "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" } - }, - "packageManager": "yarn@3.2.3" + } } diff --git a/scripts/build.ts b/scripts/build.ts index 66ecff4c..feac8e56 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -176,6 +176,7 @@ const buildmap = { "src/pyodide-worker.ts", "src/load-shinylive-sw.ts", "src/run-python-blocks.ts", + "src/lzstring-worker.ts", ], outdir: `${BUILD_DIR}/shinylive`, // See note in esbuild.build() call above about why these are external. diff --git a/site_template/editor/index.html b/site_template/editor/index.html index 5116efdb..c5a7aad0 100644 --- a/site_template/editor/index.html +++ b/site_template/editor/index.html @@ -1,4 +1,4 @@ - +
@@ -18,6 +18,13 @@ allowCodeUrl: true, allowGistUrl: true, allowExampleUrl: true, + // This option causes shinylive to update the URL hash when the user + // clicks on the re-run button in the editor. It is false by default. + // It should be set to true only when the editor and viewer are used + // in a full-window configuration. If you are using the editor and + // viewer embedded in a larger page, it does not make sense to set + // this to true. + updateUrlHashOnRerun: true, }; const appRoot = document.getElementById("root"); diff --git a/site_template/examples/index.html b/site_template/examples/index.html index 62cceba1..3f85ce36 100644 --- a/site_template/examples/index.html +++ b/site_template/examples/index.html @@ -1,4 +1,4 @@ - + @@ -15,11 +15,23 @@ // reasons, if you enable any of these, then this site should be hosted on // a separate domain or subdomain from other content. Otherwise the // running of arbitrary code could be used, for example, to steal cookies. - runApp(appRoot, "examples-editor-terminal-viewer", { - allowCodeUrl: true, - allowGistUrl: true, - allowExampleUrl: true, - }, "{{APP_ENGINE}}"); + runApp( + appRoot, + "examples-editor-terminal-viewer", + { + allowCodeUrl: true, + allowGistUrl: true, + allowExampleUrl: true, + // This option causes shinylive to update the URL hash when the user + // clicks on the re-run button in the editor. It is false by default. + // It should be set to true only when the editor and viewer are used + // in a full-window configuration. If you are using the editor and + // viewer embedded in a larger page, it does not make sense to set + // this to true. + updateUrlHashOnRerun: true, + }, + "{{APP_ENGINE}}", + ); diff --git a/src/Components/App.tsx b/src/Components/App.tsx index 1609e4ef..80570db0 100644 --- a/src/Components/App.tsx +++ b/src/Components/App.tsx @@ -91,6 +91,10 @@ type AppOptions = { // In Viewer-only mode, should the header bar be shown? showHeaderBar?: boolean; + + // When the app is re-run from the Editor, should the URL hash be updated with + // the encoded version of the app? + updateUrlHashOnRerun?: boolean; }; export type ProxyHandle = PyodideProxyHandle | WebRProxyHandle; @@ -353,6 +357,7 @@ export function App({ file.name === "app.R" || file.name === "server.R", )} + updateUrlHashOnRerun={appOptions.updateUrlHashOnRerun} appEngine={appEngine} /> @@ -400,6 +405,7 @@ export function App({ file.name === "app.R" || file.name === "server.R", )} + updateUrlHashOnRerun={appOptions.updateUrlHashOnRerun} appEngine={appEngine} /> @@ -433,6 +439,7 @@ export function App({ terminalMethods={terminalMethods} utilityMethods={utilityMethods} runOnLoad={false} + updateUrlHashOnRerun={appOptions.updateUrlHashOnRerun} appEngine={appEngine} /> @@ -458,6 +465,7 @@ export function App({ lineNumbers={false} showHeaderBar={false} floatingButtons={true} + updateUrlHashOnRerun={appOptions.updateUrlHashOnRerun} appEngine={appEngine} /> @@ -495,6 +503,7 @@ export function App({ terminalMethods={terminalMethods} utilityMethods={utilityMethods} viewerMethods={viewerMethods} + updateUrlHashOnRerun={appOptions.updateUrlHashOnRerun} appEngine={appEngine} /> diff --git a/src/Components/Editor.tsx b/src/Components/Editor.tsx index d292b91f..988226f6 100644 --- a/src/Components/Editor.tsx +++ b/src/Components/Editor.tsx @@ -11,6 +11,7 @@ import "balloon-css"; import type { Zippable } from "fflate"; import { zipSync } from "fflate"; import * as React from "react"; +import toast, { Toaster } from "react-hot-toast"; import type * as LSP from "vscode-languageserver-protocol"; import * as fileio from "../fileio"; import { createUri } from "../language-server/client"; @@ -36,7 +37,15 @@ import { languageServerExtensions } from "./codeMirror/language-server/lsp-exten import { useTabbedCodeMirror } from "./codeMirror/useTabbedCodeMirror"; import * as cmUtils from "./codeMirror/utils"; import type { FileContent } from "./filecontent"; -import { editorUrlPrefix, fileContentsToUrlString } from "./share"; +import { + editorUrlPrefix, + fileContentsToUrlString, + fileContentsToUrlStringInWebWorker, +} from "./share"; + +// If the file contents are larger than this value, then don't automatically +// update the URL hash when re-running the app. +const UPDATE_URL_SIZE_THRESHOLD = 250000; export type EditorFile = | { @@ -77,6 +86,7 @@ export default function Editor({ lineNumbers = true, showHeaderBar = true, floatingButtons = false, + updateUrlHashOnRerun = false, appEngine, }: { currentFilesFromApp: FileContent[]; @@ -93,6 +103,7 @@ export default function Editor({ lineNumbers?: boolean; showHeaderBar?: boolean; floatingButtons?: boolean; + updateUrlHashOnRerun?: boolean; appEngine: AppEngine; }) { // In the future, instead of directly instantiating the PyrightClient, it @@ -114,6 +125,33 @@ export default function Editor({ // the Viewer component. const lspPathPrefix = `editor${editorInstanceId}/`; + // This tracks whether the files have changed since the the last time the user + // has run the app/code. This is used to determine whether to update the URL. + // It is different from `setFilesHaveChanged` which is passed in, because that + // tracks whether the files have changed since they were passed into the + // Editor component. + // + // If the Editor starts with a file, then you change it and re-run, then both + // the external `filesHaveChanged` and `filesHaveChangedSinceLastRun` will be + // true. But if you re-run it again without making changes, then + // `filesHaveChanged` will still be true, and `filesHaveChangedSinceLastRun` + // will be false. + const [filesHaveChangedSinceLastRun, setFilesHaveChangedSinceLastRun] = + React.useState