DEV Community

Cover image for Power Pages SPA Learning By Doing - Keep it clean
Riccardo Gregori
Riccardo Gregori

Posted on

Power Pages SPA Learning By Doing - Keep it clean

Lately, I’ve been working a lot with Power Pages Single Page Applications (SPA).
I find this approach far more effective than the previous one, and its potential, combined with GitHub Copilot’s ability to generate React code, makes portal development faster, more efficient, and honestly even fun 😎.

Power Pages SPA reached General Availability at the beginning of February, almost two months ago, but even today the official documentation is still not fully exhaustive. Many things can only be discovered by hands-on experimentation with the platform.

This article — or rather, this series of articles — is born with the goal of sharing the pain points and hidden gems I’ve encountered during my personal journey exploring this new technology.

So… what are we waiting for?

Let’s get started! 🚀


🤔 The issue - Stale files generated by chunking strategy

If you build a Power Pages SPA using React+Vite, as in this tutorial, every time you build your portal via:

npm run build
Enter fullscreen mode Exit fullscreen mode

Build process automatically splits your site source code in a set of chunked files

Chunked files

The random-looking hashes in chunk names change because Vite uses content hashes by default. This means that when you change the content of your site, each chunk content may change, thus the chunk name changes too.

Those chunks become Web Files records in dataverse, that are created or updated when you type:

pac pages upload-code-site --rootPath .
Enter fullscreen mode Exit fullscreen mode

Exactly... that command, by itself, adds or updates Web Files, but does not remove old, now unused, files.

On the medium-long run, this is a real issue, because your site definition will be crowded by tens or thousands of useless Web Files, that you will deploy between environments.

💡 How to fix it?

📌 Step A. Apply deterministic names on chunks

To avoid generate random-looking hashed names, you can customize how Vite produces the output files, with different possible strategies:

Important notice: these strategies are tested with Vite 7.3.1. Vite 8 (released March 2026) may require a different approach.

1. Disable hashing entirely (simplest)

In vite.config.ts, override the rollupOptions.output naming:

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // Static names — no hashes at all
        entryFileNames: `assets/[name].js`,
        chunkFileNames: `assets/[name].js`,
        assetFileNames: `assets/[name].[ext]`,
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

This gives you fully predictable names like index.js, vendor.js, etc. The downside is no cache-busting, but for Power Pages/ALM this is often exactly what you want.

2. Use a fixed/stable hash (content-based, but stable)

If you want hashes but want them to only change when the content actually changes (which is actually Vite's intent, but chunk splitting can cause cascading renames):

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: `assets/[name].[hash].js`,
        chunkFileNames: `assets/[name].[hash].js`,
        assetFileNames: `assets/[name].[hash].[ext]`,
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

The issue here is usually not the hash algorithm but chunk splitting — if Rollup splits chunks differently between builds, names drift. Fix that with option 3.

3. Pin your manual chunks (most robust for ALM)

The real culprit is often Rollup's automatic code splitting producing differently-named dynamic chunks. Lock it down with manualChunks:

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        entryFileNames: `assets/[name].js`,
        chunkFileNames: `assets/[name].js`,
        assetFileNames: `assets/[name].[ext]`,
        manualChunks: {
          // Pin vendor libs into a stable named chunk
          vendor: ['react', 'react-dom'],
          // Add other large deps as needed
          // router: ['react-router-dom'],
        },
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

This way Rollup won't invent new chunk names — you've explicitly defined the boundaries.

4. Disable code splitting entirely (nuclear option)

If the SPA is small enough, just bundle everything into one file:

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        inlineDynamicImports: true, // single bundle
        entryFileNames: `assets/index.js`,
        assetFileNames: `assets/[name].[ext]`,
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Recommended approach for Power Pages

In the end I've decided to combine options 1+3, that work for my needs.
Considering that I'm using the following libraries:

  • fluentui: for ui components of my website
  • react-router-dom: to handle url routing in my SPA
  • powerbi-client-react: to embed powerbi reports on the site
  • react-google-charts: for home page charts
  • adal-angular: to manage authentication

This is (part of) my vite.config.ts:

export default defineConfig({
  plugins: [react()],
  base: '/', // absolute path - Power Pages handles routing
  build: {
    ...
    rollupOptions: {
      output: {
        entryFileNames: `assets/[name].js`,
        chunkFileNames: `assets/[name].js`,
        assetFileNames: `assets/[name].[ext]`,
        manualChunks: {
          router: ['react-router-dom'],
          fluent: ['@fluentui/react-components', '@fluentui/react-icons'],
          powerbi: ['powerbi-client-react'],
          charts: ['react-google-charts'],
          adal: ['adal-angular'],
        },
      },
    },
  },
  ...
});
Enter fullscreen mode Exit fullscreen mode

This gives me a fixed, known set of filenames that map 1:1 to my Power Pages Web Files, and they only change if you explicitly add a new manualChunks group.

Generated chunks

🧹 B. Good, but now how I get rid of the old files?

A small hint on how to fix it is given by pac pages upload-code-site command itself. When you run it, it says:

Power pages bundles

Found 10 bundle pattern(s) to match for cleanup:
1. main .*. js
3. vendor .*. js
4. vendor .*. css
5. chunk -*. js
6. chunk -*. css
7. bundle .*. js
8. bundle .*. css
9. index -*. js
10. index -*. css

Note: You can customize these patterns by adding 'bundleFilePatterns' array in 'powerpages.config.json' file.
Enter fullscreen mode Exit fullscreen mode

So, even if isn't documented anywhere, it seems it tries to clean up the mess before pushing, and it seems we can tweak the powerpages.config.json file to customize it's behavior. After a few tests, I've found that if you add a bundleFilePatterns node with an array of file patterns, those are "pruned" by pac pages upload-code-site before being replaced by the new bundles generated from your build.

In my case, the configuration that works is the following:

{
  "siteName": "my-site-name",
  "defaultLandingPage": "index.html",
  "compiledPath": ".\\dist",
  "bundleFilePatterns": [
    "main.*.js",
    "vendor.*.js",
    "vendor.*.css",
    "chunk-*.js",
    "chunk-*.css",
    "bundle.*.js",
    "bundle.*.css",
    "index-*.js",
    "index-*.css",
    "index.*.js",
    "index.*.css",
    "FluentSystemIcons-Filled.*.woff2",
    "FluentSystemIcons-Filled.*.woff",
    "FluentSystemIcons-Filled.*.ttf",
    "FluentSystemIcons-Light.*.woff2",
    "FluentSystemIcons-Light.*.woff",
    "FluentSystemIcons-Light.*.ttf",
    "FluentSystemIcons-Regular.*.woff2",
    "FluentSystemIcons-Regular.*.woff",
    "FluentSystemIcons-Regular.*.ttf",
    "FluentSystemIcons-Resizable.*.woff2",
    "FluentSystemIcons-Resizable.*.woff",
    "FluentSystemIcons-Resizable.*.ttf"
  ]
}
Enter fullscreen mode Exit fullscreen mode

And it works... it effectively removes all stale files from your local .powerpages-site folder, and when pushed to Dataverse, those file are removed from there as well... or at least, 90% of them.

🖖🏼 C. Let's finish by hand

Even after tweaking the vite.config.ts and the powerpages.config.json, I've found a few files that still remain on my Dataverse environment.

Stale files on Dataverse

Frankly I think it's a bug of PAC CLI. The only solution I've found is to remove them manually, hoping they won't be re-generated again thanks to

Please note: when you do it, you may occur in the following non-blocking-error while running:

XRM Network error: An error occurred in the PowerPageComponentDeletePlugin.
Enter fullscreen mode Exit fullscreen mode

The only solution I've found to solve this issue is to edit manually the file called .portalconfig/my-site-url-manifest.yml.

You need to:

  • use Plugin Trace Viewer XrmToolBox Plugin to get all exceptions coming from plugin Microsoft.PowerPages.Core.Plugins.PowerPageComponentDeletePlugin
  • from the trace message, extract the GUID of the missing component.

Exception

  • search for that GUID into file .portalconfig/my-site-url-manifest.yml and remove the yaml section referring to it. It should be similar to the following.
# sample block to remove
- RecordId: f9d754fb-4b71-4491-bad7-7c06eb21bce5
  DisplayName: index.COcDBgFa.css
  CheckSum: fa7ab4d0d9716e5ae809c29050b94cdaf004c4c51e1f35241716333b1352fb4a
  IsDeleted: false
Enter fullscreen mode Exit fullscreen mode

You may also want to find all lines with IsDeleted: true and remove them, and also clear the contents of the .portalconfig/manifest.yml file (that contains only references to deleted files).

🎯 Final considerations

Power Pages SPA is a huge step forward compared to the classic portal model, especially if you come from a modern frontend background. Being able to bring your own React stack, use Vite, and rely on tools like GitHub Copilot dramatically changes the developer experience and the delivery speed.

That said, today’s reality is that Power Pages SPA is still a young product. Some important behaviors — like how bundles are handled, how cleanup works, and how Dataverse Web Files are managed over time — are either underdocumented or not documented at all. As a result, production-ready solutions still require a fair amount of experimentation, reverse‑engineering, and sometimes even manual intervention.

The chunking issue described in this article is a perfect example:
nothing is technically broken, but without deterministic filenames and explicit cleanup rules, you end up with a slowly degrading site definition, polluted by stale Web Files that get deployed across environments and make ALM harder than it should be.

My personal takeaway so far is:

  • Treat the build output as a contract: Stable filenames and pinned chunks are not optional in enterprise scenarios — they’re a prerequisite.
  • Assume PAC CLI cleanup is best‑effort, not authoritative: bundleFilePatterns helps a lot, but today it doesn’t guarantee a 100% clean state.
  • Expect to occasionally "drop below the abstraction": Editing manifests and cleaning Dataverse artifacts by hand shouldn’t be necessary — but right now, it sometimes is.

Despite all of this, I still strongly believe Power Pages SPA is the right direction. The flexibility it gives you largely outweighs the current rough edges, especially if you’re already comfortable with React, modern bundlers, and CI/CD pipelines.

Hopefully, future iterations of the platform — and better documentation — will make many of the workarounds shown here obsolete. Until then, knowing where the sharp edges are is the key to using this technology effectively.

In the next articles of this series, I’ll dive into other real-world issues and patterns I’ve encountered while building Power Pages SPAs — from authentication quirks, to CSP constraints, to deployment strategies that actually scale.

Stay tuned 🚀

Top comments (0)