|
| 1 | +--- |
| 2 | +title: "Run PowerShell Data Loaders on Azure Static Web Apps" |
| 3 | +description: "Use PowerShell data loaders in Observable Framework and deploy the generated static site to Azure Static Web Apps without relying on the platform's automatic build environment." |
| 4 | +author: Andrey Vernigora |
| 5 | +authors: |
| 6 | + - Andrey Vernigora |
| 7 | +date: 2026-07-24T00:00:00+00:00 |
| 8 | +categories: |
| 9 | + - Tools |
| 10 | +tags: |
| 11 | + - powershell |
| 12 | + - observable-framework |
| 13 | + - azure-static-web-apps |
| 14 | + - github-actions |
| 15 | + - data-visualization |
| 16 | +--- |
| 17 | + |
| 18 | +Observable Framework can run PowerShell scripts during a build and expose their |
| 19 | +output to interactive pages as static data. The part that needs extra attention is |
| 20 | +deployment: the automatic Azure Static Web Apps build does not know about your custom |
| 21 | +PowerShell interpreter. |
| 22 | + |
| 23 | +This article shows how to register `.ps1` data loaders, build the site in GitHub |
| 24 | +Actions where PowerShell is available, and ask Azure Static Web Apps to deploy the |
| 25 | +already-generated files. |
| 26 | + |
| 27 | +## How Observable data loaders work |
| 28 | + |
| 29 | +[Observable Framework](https://observablehq.com/framework/) is a static site |
| 30 | +generator for data apps, dashboards, and reports. A page can load a CSV file like |
| 31 | +this: |
| 32 | + |
| 33 | +```js |
| 34 | +const movies = FileAttachment("movies.csv").csv({typed: true}); |
| 35 | +``` |
| 36 | + |
| 37 | +If `movies.csv` does not exist, Framework looks for a data loader with a double |
| 38 | +extension, such as `movies.csv.py` or `movies.csv.js`. It runs the loader during the |
| 39 | +build, saves its standard output as a static snapshot, and makes that snapshot |
| 40 | +available to the page as `movies.csv`. |
| 41 | + |
| 42 | +Framework supports several interpreters by default and lets us register additional |
| 43 | +ones. Add PowerShell to `observablehq.config.js`: |
| 44 | + |
| 45 | +```js |
| 46 | +export default { |
| 47 | + root: "src", |
| 48 | + interpreters: { |
| 49 | + ".ps1": ["pwsh"] |
| 50 | + } |
| 51 | +}; |
| 52 | +``` |
| 53 | + |
| 54 | +The `pwsh` executable must be installed and available on `PATH` wherever the site is |
| 55 | +built. |
| 56 | + |
| 57 | +## Create a PowerShell data loader |
| 58 | + |
| 59 | +Create `src/movies.csv.ps1`: |
| 60 | + |
| 61 | +```powershell |
| 62 | +$uri = 'https://raw.githubusercontent.com/vega/vega/main/docs/data/movies.json' |
| 63 | +$movies = Invoke-RestMethod -Uri $uri |
| 64 | +
|
| 65 | +$movies | |
| 66 | + Select-Object -Property Title, 'Worldwide Gross', 'US Gross', 'IMDB Rating' | |
| 67 | + ConvertTo-Csv -NoTypeInformation |
| 68 | +``` |
| 69 | + |
| 70 | +The script retrieves JSON, selects the columns needed by the page, and writes CSV to |
| 71 | +standard output. That last detail is important: standard output becomes the generated |
| 72 | +file. Send diagnostics to the information, warning, or error streams so they do not |
| 73 | +corrupt the CSV. |
| 74 | + |
| 75 | +You can test the loader independently: |
| 76 | + |
| 77 | +```powershell |
| 78 | +pwsh ./src/movies.csv.ps1 |
| 79 | +``` |
| 80 | + |
| 81 | +Then use it from `src/index.md`: |
| 82 | + |
| 83 | +````markdown |
| 84 | +```js |
| 85 | +const movies = FileAttachment("movies.csv").csv({typed: true}); |
| 86 | +``` |
| 87 | + |
| 88 | +```js |
| 89 | +Inputs.table(movies) |
| 90 | +``` |
| 91 | +```` |
| 92 | + |
| 93 | +Run the normal Framework build: |
| 94 | + |
| 95 | +```powershell |
| 96 | +npm run build |
| 97 | +``` |
| 98 | + |
| 99 | +Framework executes the PowerShell loader, caches its result, and includes the |
| 100 | +generated CSV in the static output. |
| 101 | + |
| 102 | +## Why the default Azure build can fail |
| 103 | + |
| 104 | +Azure Static Web Apps normally checks out the source and invokes its automatic build |
| 105 | +environment. That works until the application requires an interpreter or native tool |
| 106 | +that the environment does not provide or configure. |
| 107 | + |
| 108 | +Instead of teaching the automatic builder about every dependency, build the site in |
| 109 | +an ordinary GitHub Actions step and deploy only the output directory. This also makes |
| 110 | +the failing stage obvious: if a PowerShell loader breaks, the `npm run build` step |
| 111 | +fails before deployment starts. |
| 112 | + |
| 113 | +## Keep the Azure configuration in the output |
| 114 | + |
| 115 | +Azure looks for `staticwebapp.config.json` in the deployed directory. If you keep that |
| 116 | +file at the project root, copy it after the Observable build. |
| 117 | + |
| 118 | +For example, these scripts in `package.json` build the site into `dist` and copy the |
| 119 | +configuration: |
| 120 | + |
| 121 | +```json |
| 122 | +{ |
| 123 | + "scripts": { |
| 124 | + "build": "rimraf dist && observable build", |
| 125 | + "postbuild": "cp staticwebapp.config.json dist/staticwebapp.config.json" |
| 126 | + } |
| 127 | +} |
| 128 | +``` |
| 129 | + |
| 130 | +The `postbuild` script runs automatically after `npm run build`. For a |
| 131 | +cross-platform project, replace `cp` with a small Node.js script. |
| 132 | + |
| 133 | +## Build first, then deploy |
| 134 | + |
| 135 | +The relevant part of the GitHub Actions workflow looks like this: |
| 136 | + |
| 137 | +```yaml |
| 138 | +jobs: |
| 139 | + build_and_deploy: |
| 140 | + runs-on: ubuntu-latest |
| 141 | + steps: |
| 142 | + - name: Check out the repository |
| 143 | + uses: actions/checkout@v4 |
| 144 | + |
| 145 | + - name: Set up Node.js |
| 146 | + uses: actions/setup-node@v4 |
| 147 | + with: |
| 148 | + node-version: 20 |
| 149 | + cache: npm |
| 150 | + |
| 151 | + - name: Verify PowerShell |
| 152 | + shell: pwsh |
| 153 | + run: $PSVersionTable.PSVersion |
| 154 | + |
| 155 | + - name: Install dependencies |
| 156 | + run: npm ci |
| 157 | + |
| 158 | + - name: Build the Observable site |
| 159 | + run: npm run build |
| 160 | + |
| 161 | + - name: Deploy the prebuilt site |
| 162 | + uses: Azure/static-web-apps-deploy@v1 |
| 163 | + with: |
| 164 | + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} |
| 165 | + repo_token: ${{ secrets.GITHUB_TOKEN }} |
| 166 | + action: upload |
| 167 | + app_location: dist |
| 168 | + api_location: "" |
| 169 | + output_location: "" |
| 170 | + skip_app_build: true |
| 171 | +``` |
| 172 | +
|
| 173 | +The last four settings are the key: |
| 174 | +
|
| 175 | +- `app_location` points directly to the generated `dist` directory; |
| 176 | +- `output_location` is empty because no build happens inside the Azure action; |
| 177 | +- `skip_app_build` prevents the Azure action from invoking its automatic builder; |
| 178 | +- `staticwebapp.config.json` is already inside `dist`. |
| 179 | + |
| 180 | +Keep the pull-request close job and deployment token generated for your Static Web |
| 181 | +App; only replace the build-and-upload portion of the workflow. |
| 182 | + |
| 183 | +## The resulting build pipeline |
| 184 | + |
| 185 | +The finished pipeline has a simple division of responsibility: |
| 186 | + |
| 187 | +1. GitHub Actions provides Node.js and PowerShell. |
| 188 | +2. Observable Framework runs `.ps1` data loaders and produces static files. |
| 189 | +3. Azure Static Web Apps deploys those files without rebuilding them. |
| 190 | + |
| 191 | +This pattern is not limited to PowerShell. It also works when an Observable project |
| 192 | +depends on another interpreter, command-line tool, or native library that is easier to |
| 193 | +control in a dedicated build step. |
| 194 | + |
| 195 | +For more detail, see the Observable Framework documentation for |
| 196 | +[data loaders](https://observablehq.com/framework/data-loaders) and |
| 197 | +[custom interpreters](https://observablehq.com/framework/config#interpreters), plus |
| 198 | +the Azure documentation for |
| 199 | +[deploying a prebuilt application](https://learn.microsoft.com/azure/static-web-apps/build-configuration#skip-building-front-end-app). |
0 commit comments