From 1a3e8823ea19063dc8d18cd76af42b03e87d7b8a Mon Sep 17 00:00:00 2001 From: Zane Duffield Date: Fri, 7 Nov 2025 14:31:29 +1100 Subject: [PATCH 1/2] Add version selector to web demo This involves switching to dynamic loading of the WASM module, and also involves changing how the GitHub pages is built and deployed. The idea is that the latest version of the web demo site should be backwards compatible with all the previous versions of the WASM module, and the `gh-pages` branch will simply keep all the old WASM module builds, while new ones are added and the web demo site is updated. The default module version loaded is whatever is first in the versions.json file that exists at the root of the `gh-pages` branch, which the workflow makes sure to sort according to `sort -Vr` (version sort in descending order). This means that the latest tagged version should be first, and all the branches will be after all the tags. - https://www.gnu.org/software/coreutils/manual/html_node/Version-sort-overview.html In order to make switching between and comparing versions more streamlined, I also implemented a function to migrate the settings as you change the selected version. This comments and uncomments lines that correspond to settings that were removed or added when moving between versions. The selected version is also a query parameter now, so old examples shared via URL should remain stable as new versions are released. --- .github/workflows/build-web-demo/action.yaml | 14 +++ .github/workflows/deploy-web-demo.yaml | 81 +++++++++++- web/demo/index.html | 38 +++++- web/demo/public/versions.json | 1 + web/demo/src/index.ts | 125 ++++++++++++++++--- web/demo/src/vite-env.d.ts | 1 + web/src/lib.rs | 3 + 7 files changed, 241 insertions(+), 22 deletions(-) create mode 100644 web/demo/public/versions.json create mode 100644 web/demo/src/vite-env.d.ts diff --git a/.github/workflows/build-web-demo/action.yaml b/.github/workflows/build-web-demo/action.yaml index 9e15bf15..ab467b87 100644 --- a/.github/workflows/build-web-demo/action.yaml +++ b/.github/workflows/build-web-demo/action.yaml @@ -1,10 +1,24 @@ name: Build Web Demo description: Build the web demo +on: + workflow_call: + inputs: + git_ref: + description: 'The git ref to checkout and build' + required: false + default: '' runs: using: "composite" steps: + - name: Checkout repository + uses: actions/checkout@v2 + if: ${{ inputs.git_ref != '' }} + with: + fetch-depth: 0 + ref: ${{ inputs.git_ref }} + - name: Install Rust uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/deploy-web-demo.yaml b/.github/workflows/deploy-web-demo.yaml index 20e8fc87..d93a5a72 100644 --- a/.github/workflows/deploy-web-demo.yaml +++ b/.github/workflows/deploy-web-demo.yaml @@ -3,10 +3,16 @@ name: Build and Deploy Web Demo to GitHub Pages on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: + inputs: + git_ref: + description: 'The git ref to checkout and build' + required: false push: branches: - master + tags: + - v* # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: @@ -22,27 +28,94 @@ concurrency: jobs: build: runs-on: ubuntu-latest + outputs: + version: ${{ steps.set-version.outputs.VERSION }} steps: - name: Checkout repository uses: actions/checkout@v2 + - name: Determine version + id: set-version + run: | + VERSION="${{ github.event.inputs.git_ref || github.ref_name }}" + echo "VERSION=$VERSION" >> $GITHUB_ENV + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + - name: Build Web Demo uses: ./.github/workflows/build-web-demo/ + with: + git_ref: ${{ env.VERSION }} - name: Setup Pages uses: actions/configure-pages@v5 - - name: Upload artifact + - name: Upload wasm-pack artifact + uses: actions/upload-pages-artifact@v3 + with: + path: 'web/pkg/' + name: 'wasm' + + - name: Upload Vite artifact uses: actions/upload-pages-artifact@v3 with: path: 'web/demo/dist/' + name: 'vite' deploy: runs-on: ubuntu-latest needs: build + permissions: + contents: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 + - name: Set VERSION environment variable + run: echo "VERSION=${{ needs.build.outputs.VERSION }}" >> $GITHUB_ENV + + - name: Checkout gh-pages branch + uses: actions/checkout@v4 + with: + ref: gh-pages + fetch-depth: 0 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: ./ + + - name: Extract artifact + run: | + set -e + + PKG_DIR=pkg/${VERSION:?} + rm -rf "$PKG_DIR" + mkdir -p "$PKG_DIR" + tar -xvf wasm/artifact.tar -C "$PKG_DIR" --wildcards "*.js" "*.wasm" + rm wasm/artifact.tar + + tar -xvf vite/artifact.tar + rm vite/artifact.tar + + - name: Update versions.json + run: | + set -e + + readarray -t versions < <(find pkg -mindepth 1 -maxdepth 1 -type d -not -name '.*' -printf '%P\n' | sort -Vr) + echo "[$(printf '"%s",' "${versions[@]}" | sed 's/,$//')]" > versions.json + + - name: Create .nojekyll file + run: | + touch .nojekyll + + - name: Commit and push changes + run: | + set -e + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "Deploy ${VERSION:?}" || echo "No changes to commit" + git push origin gh-pages \ No newline at end of file diff --git a/web/demo/index.html b/web/demo/index.html index 62662b2f..1b934cb0 100644 --- a/web/demo/index.html +++ b/web/demo/index.html @@ -4,7 +4,7 @@ pasfmt demo - + @@ -124,7 +152,11 @@

Settings

- diff --git a/web/demo/public/versions.json b/web/demo/public/versions.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/web/demo/public/versions.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/web/demo/src/index.ts b/web/demo/src/index.ts index 63939b60..eda367f6 100644 --- a/web/demo/src/index.ts +++ b/web/demo/src/index.ts @@ -1,5 +1,3 @@ -import init, { fmt, default_settings_toml, SettingsWrapper } from "../../pkg"; - import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; // support for ini, good enough for our TOML config @@ -21,7 +19,106 @@ import "monaco-editor/esm/vs/editor/contrib/wordOperations/browser/wordOperation // custom delphi tokenizer import * as delphi from "./delphi"; -await init(); +const BASE_URL = import.meta.env.BASE_URL; + +const url = new URL(window.location.href); +const version_param = url.searchParams.get("version"); +const source_param = url.searchParams.get("source"); +const settings_param = url.searchParams.get("settings"); + +// All use of this needs to be compatible with all supported versions of the WASM module. +let pasfmt: any; + +// Load a specific version +const loadWasmVersion = async (version: string) => { + // Dynamically import the JS glue code + const mod = import.meta.env.DEV + ? await import("../../pkg/web.js") + : await import(/* @vite-ignore */ `${BASE_URL}pkg/${version}/web.js`); + + await mod.default(); + + console.log(`Loaded WASM module version: ${version}`); + + pasfmt = mod; +}; + +const loadVersion = async () => { + await loadWasmVersion(versionPicker.value); +}; + +const versionPicker = document.getElementById( + "version-picker" +)! as HTMLSelectElement; + +await fetch(`${BASE_URL}versions.json`) + .then((res) => res.json()) + .then((versions: Array) => { + versions.forEach((v) => { + const opt = document.createElement("option"); + opt.value = v; + opt.textContent = v; + versionPicker.appendChild(opt); + }); + + if (version_param !== null) { + const decoded = atob(version_param); + versionPicker.value = decoded; + if (!versionPicker.value) { + console.log(`Invalid version from search parameters: ${decoded}`); + } + } else { + versionPicker.value = versions[0]; + } + }) + .then(loadVersion); + +const showLoadingSpinner = () => { + versionPicker.disabled = true; + versionPicker.classList.add('loading') +} + +const hideLoadingSpinner = () => { + versionPicker.disabled = false; + versionPicker.classList.remove('loading') +} + +versionPicker.addEventListener("change", async () => { + showLoadingSpinner(); + await loadVersion(); + hideLoadingSpinner(); + updateSetingsOnVersionChange(); + formatEditors(); +}); + +const updateSetingsOnVersionChange = () => { + const new_valid_keys = defaultSettings() + .split("\n") + .map((line) => line.split("=")[0].trim()); + const new_settings = settingsEditor + .getValue() + .split("\n") + .map((line) => { + if (!line.includes("=")) { + return line; + } + + const commented_out = "#(unavailable)"; + if (line.startsWith(commented_out)) { + line = line.substring(commented_out.length); + } + + let key = line.split("=")[0].trim(); + if (new_valid_keys.includes(key)) { + return line; + } else { + return commented_out + line; + } + }) + .join("\n"); + + settingsEditor.setValue(new_settings); +}; const diffEditorContainer = document.getElementById("diffpane")!; const sideBySideContainer = document.getElementById("editpane")!; @@ -52,13 +149,14 @@ const settingsEditor = monaco.editor.create(settingsDiv, { const resetDefaultSettingsButton = document.getElementById( "resetToDefaultSettings" )!; -const resetSettings = () => settingsEditor.setValue(default_settings_toml()); +const defaultSettings = (): string => pasfmt.default_settings_toml(); +const resetSettings = () => settingsEditor.setValue(defaultSettings()); resetSettings(); resetDefaultSettingsButton.onclick = resetSettings; const parseSettings = () => { try { - return new SettingsWrapper(settingsEditor.getValue()); + return new pasfmt.SettingsWrapper(settingsEditor.getValue()); } catch (error) { throw new Error("Failed to parse settings", { cause: error, @@ -208,7 +306,7 @@ const formatEditors = () => { try { let settingsObj = parseSettings(); updateRulers(settingsObj.max_line_len()); - formattedModel.setValue(fmt(originalModel.getValue(), settingsObj)); + formattedModel.setValue(pasfmt.fmt(originalModel.getValue(), settingsObj)); } catch (error) { console.log(error); renderErrorInModel(error, formattedModel); @@ -245,19 +343,15 @@ document .getElementById("sample-picker")! .addEventListener("change", loadSample); -const url = new URL(window.location.href); -let source = url.searchParams.get("source"); -let settings = url.searchParams.get("settings"); - -if (source !== null) { - let decoded = atob(source); +if (source_param !== null) { + let decoded = atob(source_param); originalEditor.setValue(decoded); } else { - loadSampleFile("/pasfmt/examples/simple.pas"); + loadSampleFile(`${BASE_URL}examples/simple.pas`); } -if (settings !== null) { - let decoded = atob(settings); +if (settings_param !== null) { + let decoded = atob(settings_param); settingsEditor.setValue(decoded); } @@ -267,6 +361,7 @@ const shareExample = document.getElementById( shareExample.onclick = () => { url.searchParams.set("source", btoa(originalEditor.getValue())); url.searchParams.set("settings", btoa(settingsEditor.getValue())); + url.searchParams.set("version", btoa(versionPicker.value)); window.history.replaceState(null, "", url); navigator.clipboard.writeText(window.location.href); }; diff --git a/web/demo/src/vite-env.d.ts b/web/demo/src/vite-env.d.ts new file mode 100644 index 00000000..151aa685 --- /dev/null +++ b/web/demo/src/vite-env.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file diff --git a/web/src/lib.rs b/web/src/lib.rs index ff1030ab..d6202b7e 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -2,6 +2,9 @@ use pasfmt::{make_formatter, FormattingConfig}; use pasfmt_core::prelude::FileOptions; use wasm_bindgen::prelude::*; +// Everything here needs to be kept backwards-compatible, because the web demo +// uses the same JavaScript to interface with many versions of the WASM module. + #[wasm_bindgen] pub struct SettingsWrapper { config: FormattingConfig, From 823fbadefc85a98ffb385851936d53e7fc857b7f Mon Sep 17 00:00:00 2001 From: Zane Duffield Date: Mon, 10 Nov 2025 11:06:09 +1100 Subject: [PATCH 2/2] Fix TypeScript type hints --- web/demo/src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/demo/src/index.ts b/web/demo/src/index.ts index eda367f6..17e42f4b 100644 --- a/web/demo/src/index.ts +++ b/web/demo/src/index.ts @@ -168,10 +168,10 @@ const closeModalButton = document.getElementById( "closeModal" ) as HTMLButtonElement; -const setSettingsBorderCol = (col) => +const setSettingsBorderCol = (col: string) => document.documentElement.style.setProperty("--settings-border", col); -let settingsErrorTimeout; +let settingsErrorTimeout: number; let settingsValid = true; settingsModel.onDidChangeContent(() => { clearTimeout(settingsErrorTimeout); @@ -273,7 +273,7 @@ document .getElementById("toggle-view")! .addEventListener("click", toggleDiffView); -const renderErrorInModel = (error, model) => { +const renderErrorInModel = (error: any, model: monaco.editor.ITextModel) => { const errorMessage = error + (error.cause ? `\nCaused by: ${error.cause}` : ""); const markers = [ @@ -290,11 +290,11 @@ const renderErrorInModel = (error, model) => { monaco.editor.setModelMarkers(model, "", markers); }; -const clearErrorsInModel = (model) => { +const clearErrorsInModel = (model: monaco.editor.ITextModel) => { monaco.editor.setModelMarkers(model, "", []); }; -const updateRulers = (maxLineLen) => { +const updateRulers = (maxLineLen: number) => { originalEditor.updateOptions({ rulers: [makeRuler(maxLineLen)] }); formattedEditor.updateOptions({ rulers: [makeRuler(maxLineLen)] }); if (diffEditor !== undefined) {