Download scripts/build/check_reader_release.mjs from VoiceOfML/Search: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/scripts/build/check_reader_release.mjs
- Command line
-
hf download hf://spaces/VoiceOfML/Search/scripts/build/check_reader_release.mjs
-
curl -L -o check_reader_release.mjs https://huggingface.co/spaces/VoiceOfML/Search/resolve/main/scripts/build/check_reader_release.mjs
10.7 kB
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import crypto from "node:crypto"; | |
| import { createRequire } from "node:module"; | |
| const resources = createRequire(import.meta.url)( | |
| path.join(process.cwd(), "static/reader-resources.js") | |
| ); | |
| const args = Object.fromEntries( | |
| process.argv | |
| .slice(2) | |
| .reduce( | |
| (items, value, index, all) => | |
| value.startsWith("--") | |
| ? [...items, [value.slice(2), all[index + 1]?.startsWith("--") ? true : all[index + 1]]] | |
| : items, | |
| [] | |
| ) | |
| ); | |
| const sourceRequired = ["reader.html", ...resources.readerFiles]; | |
| const required = [...sourceRequired, ...resources.foliateFiles.map(file => `foliate-reader/${file}`)]; | |
| // Source smoke assertions, not a substitute for runtime behavior tests. | |
| const behavioral = [ | |
| /\bVoiceOfMLReaderRuntime\s*\.\s*createReaderRuntime\s*\(/, | |
| /\bVoiceOfMLReaderAdapters\s*\.\s*createAdapterRegistry\s*\(/, | |
| /\bformatAdapters\s*\.\s*register\s*\(\s*mode\s*,/, | |
| /\bformatAdapters\s*\.\s*activate\s*\(\s*capability\s*\.\s*mode\s*\)/, | |
| /\bdisableStream\s*:\s*(?:true|false)\b/, | |
| /\bnextReaderGeneration\s*\(\s*["']navigation["']\s*\)/ | |
| ]; | |
| const hash = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); | |
| function assert(condition, message) { | |
| if (!condition) throw new Error(message); | |
| } | |
| function staticRoot(root, artifact) { | |
| if (!artifact) return path.join(root, "static"); | |
| return fs.existsSync(path.join(artifact, "static")) ? path.join(artifact, "static") : artifact; | |
| } | |
| function checkBehavior(root) { | |
| const reader = ["reader.js", ...resources.readerComponentFiles].map(file => | |
| fs.readFileSync(path.join(root, "static", file), "utf8")).join("\n").replace(/\breaderContext\./g, ""); | |
| for (const [, dependency] of reader.matchAll(/(?:\/static\/)([^"'\s]+\.(?:js|mjs))/g)) { | |
| assert( | |
| required.includes(dependency), | |
| `Reader dependency missing from inventory: ${dependency}` | |
| ); | |
| } | |
| for (const marker of behavioral) { | |
| assert(marker.test(reader), `missing Reader source behavior in ${root}: ${marker}`); | |
| } | |
| } | |
| function checkAssets(output, hashed = false) { | |
| for (const vendor of Object.values(resources.foliateVendors)) { | |
| const filename = path.join("foliate-reader/vendor", vendor.file); | |
| assert(hash(path.join(output, filename)) === vendor.sha256, | |
| `Foliate vendor integrity mismatch: ${filename}`); | |
| } | |
| for (const [name, filename] of [["pdf", "pdf.mjs"], ["pdfWorker", "pdf.worker.mjs"]]) { | |
| assert(hash(path.join(output, "foliate-reader/vendor/pdfjs", filename)) === resources.vendors[name].sha256, | |
| `Foliate PDF engine mismatch: ${filename}`); | |
| } | |
| for (const vendor of Object.values(resources.vendors)) { | |
| assert( | |
| hash(path.join(output, vendor.path)) === vendor.sha256, | |
| `vendor integrity mismatch: ${vendor.path}` | |
| ); | |
| } | |
| const files = new Set([...required, "app.js"].map(file => hashed ? currentAsset(output, file) : file)); | |
| for (const file of files) { | |
| const target = path.join(output, file); | |
| assert(fs.existsSync(target) && fs.statSync(target).isFile(), `missing Reader asset: ${file}`); | |
| const shortHash = path.basename(file).match(/\.([0-9a-f]{12})\.(?:js|mjs|css)$/)?.[1]; | |
| if (shortHash) { | |
| assert(hash(target).startsWith(shortHash), `Reader asset hash mismatch: ${file}`); | |
| } | |
| // Vendor internals may contain optional paths; inspect our references to them only. | |
| if (file.split(path.sep).includes("vendor")) continue; | |
| const content = fs.readFileSync(target, "utf8"); | |
| const refs = content.matchAll(/(["'])((?:\/(?:search\/)?static\/|\.\.?\/)[^"'\s]+)\1/g); | |
| for (const [, , url] of refs) { | |
| const ref = url.split(/[?#]/)[0]; | |
| if (!ref.startsWith("/") && !/\.(?:js|mjs|css|html)$/.test(ref)) continue; | |
| const relative = ref.startsWith("/") | |
| ? ref.replace(/^\/(?:search\/)?static\//, "") | |
| : path.join(path.dirname(file), ref); | |
| const resolved = path.resolve(output, relative); | |
| assert( | |
| resolved.startsWith(`${path.resolve(output)}${path.sep}`), | |
| `invalid Reader asset: ${url}` | |
| ); | |
| assert(fs.existsSync(resolved), `unresolved Reader reference in ${file}: ${url}`); | |
| if (ref.endsWith("/")) { | |
| assert( | |
| fs.statSync(resolved).isDirectory() && fs.readdirSync(resolved).length > 0, | |
| `empty or invalid Reader asset directory: ${url}` | |
| ); | |
| } else { | |
| if (hashed && resources.buildFiles("github").includes(relative)) { | |
| throw new Error(`unversioned build dependency in ${file}: ${url}`); | |
| } | |
| files.add(relative); | |
| } | |
| } | |
| } | |
| for (const directory of ["vendor/cmaps", "vendor/standard_fonts", "vendor/wasm"]) { | |
| const target = path.join(output, directory); | |
| assert( | |
| fs.existsSync(target) && | |
| fs.statSync(target).isDirectory() && | |
| fs.readdirSync(target).length > 0, | |
| `missing or empty Reader vendor directory: ${directory}` | |
| ); | |
| } | |
| for (const directory of ["foliate-reader/vendor/pdfjs/cmaps", "foliate-reader/vendor/pdfjs/standard_fonts", | |
| "foliate-reader/vendor/pdfjs/wasm"]) { | |
| const target = path.join(output, directory); | |
| assert(fs.existsSync(target) && fs.statSync(target).isDirectory() && fs.readdirSync(target).length > 0, | |
| `missing or empty Foliate PDF.js directory: ${directory}`); | |
| } | |
| } | |
| function currentAsset(output, filename) { | |
| if (!resources.buildFiles("github").includes(filename)) return filename; | |
| assert(!fs.existsSync(path.join(output, filename)), `obsolete asset alias: ${filename}`); | |
| const directory = path.dirname(filename), basename = path.basename(filename); | |
| const extension = path.extname(basename), stem = basename.slice(0, -extension.length); | |
| const matches = fs.readdirSync(path.join(output, directory)).filter(file => file.startsWith(stem + ".") && | |
| file.endsWith(extension) && /^[0-9a-f]{12}$/.test(file.slice(stem.length + 1, -extension.length))); | |
| assert(matches.length === 1, `expected one current asset: ${filename}`); | |
| return path.join(directory, matches[0]); | |
| } | |
| function checkProject(root, artifact) { | |
| const source = path.join(root, "static"); | |
| for (const file of sourceRequired) { | |
| const target = path.join(source, file); | |
| assert( | |
| fs.existsSync(target) && fs.statSync(target).isFile(), | |
| `missing source Reader file: ${file}` | |
| ); | |
| } | |
| checkBehavior(root); | |
| const output = staticRoot(root, artifact); | |
| checkAssets(output, !!artifact); | |
| if (artifact) { | |
| const project = args.project || (fs.existsSync(path.join(root, "Dockerfile")) ? "hf" : "github"); | |
| for (const file of resources.buildFiles(project)) currentAsset(output, file); | |
| const shellPath = fs.existsSync(path.join(output, "index.html")) | |
| ? path.join(output, "index.html") : path.join(output, "../index.html"); | |
| const shell = fs.readFileSync(shellPath, "utf8"); | |
| const scripts = [...shell.matchAll(/<script\b[^>]*\bsrc="([^"]+)"/g)].map(match => match[1]); | |
| assert(scripts.length === 1 && /\/app\.[0-9a-f]{12}\.js$/.test(scripts[0]), | |
| "search shell must load one hashed app bundle"); | |
| const app = fs.readFileSync(path.join(output, currentAsset(output, "app.js")), "utf8"); | |
| const swPath = fs.existsSync(path.join(output, "sw.js")) | |
| ? path.join(output, "sw.js") | |
| : path.join(output, "../sw.js"); | |
| const sw = fs.readFileSync(swPath, "utf8"); | |
| assert( | |
| !/VoiceOfMLReaderResources\.(?:shellAssets|engineAssets)\(/.test(app), | |
| "unexpanded Reader warming inventory" | |
| ); | |
| assert( | |
| !/VoiceOfMLReaderResources\.runtimePaths\(/.test(sw), | |
| "unexpanded Reader cache inventory" | |
| ); | |
| } | |
| const manifest = [ | |
| ...sourceRequired.map(file => `${file}:${hash(path.join(source, file))}`), | |
| ...resources.foliateFiles.map(file => `foliate-reader/${file}:${hash(path.join(output, "foliate-reader", file))}`) | |
| ].join("\n"); | |
| return crypto.createHash("sha256").update(manifest).digest("hex"); | |
| } | |
| function crossCheck(githubRoot, hfRoot) { | |
| const exact = [ | |
| "reader-resources.js", | |
| "reader/core/reader-store.js", | |
| "reader/pdf/reader-pdf-text.js", | |
| "reader/search/workers/reader-pdf-book-search-worker.mjs", | |
| "reader/search/reader-pdf-book-search.mjs", | |
| "search/search-session.js", | |
| "search/download-controller.js", | |
| "search/reader-navigation.js", | |
| "reader.css", | |
| "reader/core/reader-request-manager.js", | |
| "reader/formats/reader-chapter-repository.js", | |
| "reader/ui/reader-scroll-anchor.js", | |
| "reader/formats/reader-section-virtualizer.js", | |
| "reader/core/reader-runtime.js", | |
| "reader/formats/reader-format-adapters.js", | |
| "reader/core/reader-security.js", | |
| "reader/search/reader-search-controller.mjs", | |
| "reader/search/reader-dom-search.mjs", | |
| "reader/formats/reader-image-loader.mjs", | |
| "reader/reader-components.mjs", | |
| "reader/ui/reader-document-styles.mjs", | |
| "reader/core/reader-diagnostics.mjs", "reader/core/reader-lifecycle.mjs", "reader/ui/reader-bookmarks.mjs", | |
| "reader/formats/reader-media-format.mjs", "reader/core/reader-document-loader.mjs", "reader/formats/reader-html-format.mjs", | |
| "reader/formats/reader-text-format.mjs", "reader/formats/reader-chapter-format.mjs", | |
| "reader/pdf/pdf-worker-wrapper.mjs" | |
| ]; | |
| for (const file of exact) { | |
| assert( | |
| hash(path.join(githubRoot, "static", file)) === hash(path.join(hfRoot, "static", file)), | |
| `cross-project drift: ${file}` | |
| ); | |
| } | |
| for (const file of ["compose_app.mjs", "copy_reader_vendor.mjs", "fetch_foliate_reader.mjs", "foliate-local.patch.b64", "check_reader_release.mjs"]) { | |
| assert( | |
| hash(path.join(githubRoot, "scripts/build", file)) === hash(path.join(hfRoot, "scripts/build", file)), | |
| `cross-project drift: scripts/build/${file}` | |
| ); | |
| } | |
| for (const root of [githubRoot, hfRoot]) checkBehavior(root); | |
| const vectors = "tests/reader-contract-vectors.json"; | |
| if (fs.existsSync(path.join(githubRoot, vectors)) && fs.existsSync(path.join(hfRoot, vectors))) { | |
| assert( | |
| hash(path.join(githubRoot, vectors)) === hash(path.join(hfRoot, vectors)), | |
| "cross-project drift: Reader contract vectors" | |
| ); | |
| } | |
| } | |
| try { | |
| if (args.cross) { | |
| crossCheck(path.resolve(args["github-root"]), path.resolve(args["hf-root"])); | |
| console.log("reader cross-project release gate ok"); | |
| } else { | |
| const root = path.resolve(args.root || "."); | |
| const identity = checkProject( | |
| root, | |
| args["artifact-root"] ? path.resolve(args["artifact-root"]) : null | |
| ); | |
| console.log(`reader release gate ok ${identity}`); | |
| } | |
| } catch (error) { | |
| console.error(`reader release gate failed: ${error.message}`); | |
| process.exitCode = 1; | |
| } | |