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..17e42f4b 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, @@ -70,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); @@ -175,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 = [ @@ -192,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) { @@ -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,