Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/prettier.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: 'Lint and Format'
name: 'Check formatting'

# This workflow runs on pushes to any branch
# and on any pull request
Expand All @@ -7,7 +7,7 @@ on:

jobs:
prettier-format-commit:
name: Prettier format and commit
name: Check prettier formatting
runs-on: ubuntu-latest

steps:
Expand Down
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# IWA project templates

This project will contain templates that could be used for creation of new Isolated Web Apps (IWAs) projects.
This project will contain templates that could be used for creation of new Isolated Web Apps (IWAs)
projects.

## Prerequisites

- Google Chrome 120+
- Google Chrome 140+

- openssl

Expand All @@ -23,7 +25,7 @@ Open Google Chrome, go to `chrome://flags` and enable

chrome://flags/#enable-isolated-web-app-dev-mode

chrome://flags/#enable-isolated-web-apps
chrome://flags/#enable-isolated-web-apps (MacOS/Windows/Linux only)

Once enabled, and chrome is restarted, go to `chrome://web-app-internals`, the web app internals
page. This page shows you the web apps you have installed, including Isolated Web Apps and
Expand Down Expand Up @@ -82,7 +84,8 @@ Next, create a .env file containing:
PRIVATE_KEY_PASSWORD='YOUR_KEY_PASSWORD'
PORT=YOUR_PORT

_Note: If you're using vite, also declare a variable called NODE_ENV, you can set it to either production/development_
_Note: If you're using vite, also declare a variable called NODE_ENV, you can set it to either
production/development_

_Note: If you don't specify PORT, the app will fall back to default value (4321)_

Expand All @@ -105,9 +108,8 @@ slu74sbybztfypa43w7f7rd34cbhautjcrfegz5lbow7vmwjojbqaaic).

_Note: Your web bundle ID will look differently, this is just an example_

Since IWA are using a different schema `isolated-app://` instead of `https://`, you can access it by
pasting `isolated-app://your-web-bundle-id` in Chrome, or by running it like any other app on your
computer.
To run your newly installed Isolated Web App, use chrome://apps (Windows/Mac/Linux only) or by
running it like any other application on your desktop.

### Personalizing this template

Expand All @@ -126,7 +128,6 @@ Related information:
Isolated Web Apps share the same
[Manifest Properties](https://web.dev/articles/add-manifest#manifest-properties) as Progressive Web
Apps, with some slight variations, see:
[Isolated Web Apps explainer - Web App manifest section](https://chromeos.dev/en/web/isolated-web-apps)

There are two fields that should be included, `version` and `update_manifest_url`

Expand All @@ -139,8 +140,8 @@ There are two fields that should be included, `version` and `update_manifest_url

- How to add API Permissions?

By default, Chrome blocks all permission requests from IWAs. You can opt-in to permission policies you need by specifying
a `permissions_policy` field in your manifest.
By default, Chrome blocks all permission requests from IWAs. You can opt-in to permission policies
you need by specifying a `permissions_policy` field in your manifest.

_Note: Adding a permission here does not automatically grant it, it just makes it avaliable to be
granted, when a request for that capacity is made._
Expand All @@ -155,7 +156,7 @@ granted, when a request for that capacity is made._
Related Information

- [Permissions Policy - MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Permissions_Policy#allowlists)
- [Permissions Policy header - MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Permissions_Policy#allowlists)
- [Permissions Policy header - MDN Docs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy)
- [Web Application Manifest - MDN Docs](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest)

### Related Information
Expand Down
17 changes: 17 additions & 0 deletions vite-iwa-template/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions vite-iwa-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
"prettier": "prettier -w ."
},
"devDependencies": {
"@types/trusted-types": "^2.0.7",
"dompurify": "^3.2.6",
"dotenv": "^17.2.0",
"prettier": "^3.6.2",
"rollup-plugin-webbundle": "^0.2.0",
Expand Down
42 changes: 35 additions & 7 deletions vite-iwa-template/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may not use a copy of the License at
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
Expand All @@ -15,21 +15,33 @@
*/

import "./style.css";
import DOMPurify from "dompurify";

const parent_container = document.querySelector("#app") as HTMLDivElement;

function deleteChildNodes(container: HTMLDivElement): void {
while (container.firstChild) {
container.removeChild(container.firstChild);
}
let sanitizerPolicy: any = null!;
let urlPolicy: any = null!;

// This block will always execute in Chrome, so the policies will be assigned.
if (window.trustedTypes && window.trustedTypes.createPolicy) {
// Policy for sanitizing HTML strings before assigning them to innerHTML.
sanitizerPolicy = window.trustedTypes.createPolicy("iwa-sanitizer", {
createHTML: (string) => DOMPurify.sanitize(string),
});

// Policy for creating TrustedScriptURL. This is required for navigator.serviceWorker.register().
// For this app, we can just pass the URL through, as we know it's safe.
urlPolicy = window.trustedTypes.createPolicy("iwa-url-policy", {
createScriptURL: (url) => url,
});
}

/**
* Loads images and other content when the app is online.
* It first clears the container to prevent duplicate content.
*/
function loadOnlineContent() {
deleteChildNodes(parent_container);
parent_container.innerHTML = sanitizerPolicy.createHTML("");

const icon_elements: [string, string][] = [
[
Expand Down Expand Up @@ -74,7 +86,7 @@ function loadOnlineContent() {
* Displays an offline message in the app container.
*/
function showOfflineMessage() {
deleteChildNodes(parent_container);
parent_container.innerHTML = sanitizerPolicy.createHTML("");
const p_offline = document.createElement("p");

const title = document.createElement("h1");
Expand All @@ -87,6 +99,22 @@ function showOfflineMessage() {
parent_container.appendChild(p_offline);
}

window.addEventListener("load", () => {
if ("serviceWorker" in navigator) {
// Use the trusted type policy to create a TrustedScriptURL.
const swUrl = urlPolicy.createScriptURL("./service-worker.js");

navigator.serviceWorker
.register(swUrl)
.then((registration) => {
console.log("Service Worker registered with scope:", registration.scope);
})
.catch((error) => {
console.error("Service Worker registration failed:", error);
});
}
});

window.addEventListener("online", () => {
loadOnlineContent();
});
Expand Down
92 changes: 92 additions & 0 deletions vite-iwa-template/src/service-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/// <reference lib="webworker" />

declare const self: ServiceWorkerGlobalScope;

const CACHE_NAME = "iwa-image-cache-v1";

const URLS_TO_CACHE = [
"https://upload.wikimedia.org/wikipedia/commons/f/f1/Vitejs-logo.svg",
"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4c/Typescript_logo_2020.svg/250px-Typescript_logo_2020.svg.png",
"https://upload.wikimedia.org/wikipedia/commons/e/e1/Google_Chrome_icon_%28February_2022%29.svg",
];

/**
* Installation event.
*/
self.addEventListener("install", (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log("Opened cache");
return cache.addAll(URLS_TO_CACHE);
}),
);
});

/**
* Fetch event.
*/
self.addEventListener("fetch", (event: FetchEvent) => {
if (!event.request.url.startsWith("http")) {
return;
}

event.respondWith(
caches.match(event.request).then((response) => {
if (response) {
return response;
}

return fetch(event.request).then((networkResponse) => {
if (
!networkResponse ||
networkResponse.status !== 200 ||
(networkResponse.type !== "basic" && networkResponse.type !== "cors")
) {
return networkResponse;
}

const responseToCache = networkResponse.clone();

caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseToCache);
});

return networkResponse;
});
}),
);
});

/**
* Activate event.
*/
self.addEventListener("activate", (event: ExtendableEvent) => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
}),
);
}),
);
});
1 change: 1 addition & 0 deletions vite-iwa-template/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"types": ["trusted-types"],
"target": "ES2022",
"useDefineForClassFields": true,
"module": "ESNext",
Expand Down
12 changes: 12 additions & 0 deletions vite-iwa-template/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import { defineConfig } from "vite";
import injectHTML from "vite-plugin-html-inject";
import fs from "fs";
import { resolve } from "path";

import wbn from "rollup-plugin-webbundle";
import * as wbnSign from "wbn-sign";
Expand Down Expand Up @@ -77,6 +78,17 @@ export default defineConfig({
rollupOptions: {
input: {
main: "./index.html",
"service-worker": resolve(__dirname, "src/service-worker.ts"),
},

output: {
entryFileNames: (chunkInfo) => {
if (chunkInfo.name === "service-worker") {
return "service-worker.js";
}

return "assets/[name]-[hash].js";
},
},
},
},
Expand Down
17 changes: 17 additions & 0 deletions webpack-iwa-template/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions webpack-iwa-template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
},
"license": "Apache-2.0",
"devDependencies": {
"@types/trusted-types": "^2.0.7",
"copy-webpack-plugin": "^13.0.0",
"css-loader": "^7.1.2",
"dompurify": "^3.2.6",
"dotenv": "^17.2.0",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.6.3",
Expand Down
Loading