Skip to content
Open
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
60 changes: 60 additions & 0 deletions lib/domstack-manifest/hash.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @import { DomstackManifestEntry } from './schema.js'
*/
import { createHash } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import { stableJsonStringify } from '../helpers/stable-json-stringify.js'

/**
* Hash only the fields that affect static cache membership and content. Source
* metadata is intentionally ignored so debug/build-origin changes do not churn
* PWA cache names.
*
* @param {{ update: (value: string) => unknown }} hash
* @param {DomstackManifestEntry} entry
*/
export function updateManifestVersionHash (hash, entry) {
hash.update(entry.url)
hash.update('\0')
hash.update(entry.revision ?? '')
hash.update('\0')
hash.update(entry.kind)
hash.update('\0')
updateManifestVersionValue(hash, entry.contentType)
updateManifestVersionValue(hash, entry.integrity)
updateManifestVersionValue(hash, entry.manifestVars)
updateManifestVersionValue(hash, entry.urlRevisioned)
updateManifestVersionValue(hash, entry.static)
updateManifestVersionValue(hash, entry.role)
}

/**
* @param {{ update: (value: string) => unknown }} hash
* @param {unknown} value
*/
export function updateManifestVersionValue (hash, value) {
const serializedValue = stableJsonStringify(value)
hash.update(serializedValue ?? '')
hash.update('\0')
}

/**
* @param {string} filepath
*/
export async function hashFileDigest (filepath) {
const contents = await readFile(filepath)
const digest = createHash('sha256').update(contents).digest()
return {
hex: digest.toString('hex'),
integrity: `sha256-${digest.toString('base64')}`,
}
}

/**
* @param {string | null} revision
* @returns {string | undefined}
*/
export function revisionToIntegrity (revision) {
if (!revision || !/^[a-f0-9]{64}$/i.test(revision)) return undefined
return `sha256-${Buffer.from(revision, 'hex').toString('base64')}`
}
60 changes: 60 additions & 0 deletions lib/domstack-manifest/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @import { DomstackManifest, DomstackManifestOptions, DomstackManifestBuiltHookResult } from './schema.js'
*/
import { mkdir, writeFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { assertInsideDest } from '../helpers/path.js'
import { DEFAULT_DOMSTACK_MANIFEST_FILENAME } from './schema.js'

/**
* @param {string} dest
* @param {DomstackManifest} domstackManifest
*/
export async function writeDomstackManifest (dest, domstackManifest) {
const manifestPath = resolve(dest, DEFAULT_DOMSTACK_MANIFEST_FILENAME)
assertInsideDest(dest, manifestPath)
await mkdir(dirname(manifestPath), { recursive: true })
await writeFile(manifestPath, JSON.stringify(domstackManifest, null, 2))
}

/**
* @param {string} dest
* @param {string} outputRelname
* @param {string | Uint8Array} contents
*/
async function writeGeneratedManifestFile (dest, outputRelname, contents) {
const filepath = resolve(dest, outputRelname)
assertInsideDest(dest, filepath)
await mkdir(dirname(filepath), { recursive: true })
await writeFile(filepath, contents)
}

/**
* Run generated-artifact hooks after the domstack manifest has been built.
*
* @param {string} dest
* @param {DomstackManifest} manifest
* @param {DomstackManifestOptions} options
* @returns {Promise<DomstackManifestBuiltHookResult>}
*/
export async function runDomstackManifestBuiltHooks (dest, manifest, options) {
const serviceWorkerDefines = /** @type {Record<string, string>} */ ({})
const hooks = options.hooks?.manifestBuilt ?? []

for (const hook of hooks) {
await hook({
dest,
manifest,
defineServiceWorkerConstant: (identifier, value) => {
const serializedValue = JSON.stringify(value)
if (serializedValue === undefined) {
throw new TypeError(`Service-worker define ${identifier} must be JSON-serializable`)
}
serviceWorkerDefines[identifier] = serializedValue
},
writeFile: (outputRelname, contents) => writeGeneratedManifestFile(dest, outputRelname, contents),
})
}

return { serviceWorkerDefines }
}
Loading