Skip to content

hazae41/glace

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

225 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Glace

Build a webapp with security in mind

npm install -D @hazae41/glace

📦 NPM

Features

  • Built on web standards and fundamentals
  • Works with any client-side and/or static-side library (e.g. React, jQuery)
  • Focused on security and simplicity without degrading performances
  • Supply-chain hardened with the bare minimum dependencies
  • Built for Deno but backward compatible with Node/Bun
  • Deploy it anywhere HTML/CSS/JS files can be stored
  • Builds are cross-platform cross-runtime reproducible

Starters

Usage

Bash

glace ./www --out=./dst --dev --watch=./www,./src

Code

await new Glace("./www", "./dst", "production").build()

Features

.bundleignore

You can put a .bundleignore file at the root of your input directory to ignore some files

./www/.bundleignore

manifest.json
/assets/*

Those files will be copied as-is without any bundling

JS/TS(X), CSS, JSON files

Those files will be bundled for the client unless explicitly ignored (see previous section)

HTML files

Scripts

Any <script> will be bundled and then executed with the HTML file set for document and location

<script type="module">
  document.body.innerHTML = `<div>Built at ${Date.now()}</div>`
</script>

You can branch on browser or static execution using process.env.PLATFORM

<script type="module">
  if (process.env.PLATFORM === "browser") {
    console.log("Hello from browser")
  } else {
    console.log("Hello from bundler")
  }
</script>

Subresource Integrity

All scripts, whether inline or external, will have their integrity attribute automatically computed.

<script type="module" src="./index.tsx"></script>
<script type="module" src="./index.js" integrity="sha256-xP+cym0GRdm2J0F0v39EBGjOtHbuY8qEHoeQrqrhgcs="></script>

External scripts will also be included in a modulepreload link

<link rel="modulepreload" href="./index.js" integrity="sha256-xP+cym0GRdm2J0F0v39EBGjOtHbuY8qEHoeQrqrhgcs=" />

And an importmap will be generated with the integrity of external scripts

<script type="importmap">{"integrity":{"./index.js":"sha256-xP+cym0GRdm2J0F0v39EBGjOtHbuY8qEHoeQrqrhgcs="}}</script>

Service-worker

If you set background.service_worker to an output file path in manifest.json, it will be injected by replacing FILES by a [string, string][] mapping of all files and their integrity

{
  "name": "Example",
  "short_name": "Example",
  "description": "An example webapp",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#ffffff",
  "icons": [
    {
      "src": "/favicon.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "background": {
    "service_worker": "/service.worker.js"
  }
}

You can then use FILES to cache all your webapp files

import { immutable } from "@hazae41/immutable"

declare const FILES: [string, string][]

const cache = new immutable.cache.Cache(new Map(FILES))

self.addEventListener("install", (event) => {
  /**
   * Precache new version and auto-activate
   */
  event.waitUntil(cache.precache().then(() => self.skipWaiting()))
})

self.addEventListener("activate", (event) => {
  /**
   * Take control of all clients and uncache previous versions
   */
  event.waitUntil(self.clients.claim().then(() => cache.uncache()))
})

self.addEventListener("fetch", (event) => {
  const response = cache.handle(event.request)

  if (response == null)
    return

  /**
   * Respond with cache
   */
  event.respondWith(response)
})

Generated paths

You can generate paths using brackets as parameters

And parameters will be available in location as search params

./www/[lang].html

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Example</title>
  <script type="module">
    if (process.env.PLATFORM !== "browser") {
      document.documentElement.lang = new URLSearchParams(location.search).get("lang")
    }
  </script>
</head>

</html>

./www/manifest.json

{
  "short_name": "Example",
  "name": "Example",
  "paths": [
    {
      "lang": "en"
    },
    {
      "lang": "fr"
    },
    {
      "lang": "es"
    },
    {
      "lang": "de"
    }
  ]
}

Will output

  • ./out/en.html

  • ./out/fr.html

  • ./out/es.html

  • ./out/de.html

And you can use multiple parameters

./www/posts/[post]/[name].html

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Example</title>
  <script type="module">
    if (process.env.PLATFORM !== "browser") {
      const params = new URLSearchParams(location.search)

      document.title = params.get("name")

      document.body.innerHTML = await fetch(`/api/posts/${params.get("post")}`).then(r => r.text())
    }
  </script>
</head>

</html>

./www/manifest.json

{
  "short_name": "Example",
  "name": "Example",
  "paths": [
    {
      "post": 1,
      "name": "how-to-start-coding"
    },
    {
      "post": 2,
      "name": "how-to-choose-a-license"
    },
    {
      "post": 3,
      "name": "how-to-deploy-a-website"
    }
  ]
}

Will output

  • ./out/posts/1/how-to-start-coding.html

  • ./out/posts/2/how-to-choose-a-license.html

  • ./out/posts/3/how-to-deploy-a-website.html

You can write your own script that will fetch your database and fill manifest.json

./scripts/generate.ts

const manifest = await readFile("./www/manifest.json", "utf-8").then(JSON.parse)

const posts = await fetch("https://example.com/api/posts").then(r => r.json())

for (const { id, title } of posts)
  manifest.paths.push[{ post: id, name: title.replaceAll(" ", "-").toLowerCase() }]

await writeFile("./www/manifest.json", JSON.stringify(manifest, null, 2))
deno -RWN ./scripts/generate.ts

Examples

A simple HTML file with prerendering

./www/index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Example</title>
  <script type="module">
    if (process.env.PLATFORM !== "browser") 
      document.body.innerHTML = `<div>Copyright ${new Date().getUTCFullYear()}</div>`
    }
  </script>
</head>

</html>

Will output

<!DOCTYPE html>
<html lang="en">
  
<head>
  <title>Example</title>
</head>

<body>
  <div>Copyright 2025</div>
</body>

</html>

A simple HTML file with prerendering and hydration

./www/index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Example</title>
  <script type="module">
    document.body.innerHTML = `<div>Copyright ${new Date().getUTCFullYear()}</div>`
  </script>
</head>

</html>

Will output

<!DOCTYPE html>
<html lang="en">
  
<head>
  <title>Example</title>
  <script type="module">
    document.body.innerHTML = `<div>Copyright ${new Date().getUTCFullYear()}</div>`
  </script>
</head>

<body>
  <div>Copyright 2025</div>
</body>

</html>

A simple React and Tailwind app with prerendering and hydration using Rewind

npm i react react-dom @types/react @types/react-dom @hazae41/rewind

./www/index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Example</title>
  <script type="module" src="./index.tsx"></script>
  <link rel="stylesheet" data-rewind href="./index.css" />
</head>

</html>

./www/index.css

@import "tailwindcss/index";

.big {
  @apply text-3xl;
}

./www/index.tsx

import { Rewind } from "@hazae41/rewind";
import React, { type ReactNode, useEffect } from "react";
import { hydrateRoot } from "react-dom/client";

React;

export function App() {
  useEffect(() => {
    console.log("Hello world");
  }, [])

  return <div className="text-red-500 big">
    Hello world
  </div>
}

if (process.env.PLATFORM === "browser") {
  await new Rewind(document).hydrateOrThrow().then(() => hydrateRoot(document.body, <App />))
} else {
  const prerender = async (node: ReactNode) => {
    const ReactDOM = await import("react-dom/static")

    using stack = new DisposableStack()

    const stream = await ReactDOM.default.prerender(node)
    const reader = stream.prelude.getReader()

    stack.defer(() => reader.releaseLock())

    let html = ""

    for (let result = await reader.read(); !result.done; result = await reader.read())
      html += new TextDecoder().decode(result.value)

    return html
  }

  document.body.innerHTML = await prerender(<App />)

  await new Rewind(document).prerenderOrThrow()
}

For more complete examples, see #starters

About

Build a webapp with security in mind

Resources

License

Stars

Watchers

Forks

Sponsor this project

Packages

 
 
 

Contributors