hypertwist/Content/Browser/scripts/verify-browser-shell.mjs
2026-07-23 17:23:24 +00:00

238 lines
8.1 KiB
JavaScript

import { existsSync, readFileSync } from 'node:fs';
import path from 'node:path';
import process from 'node:process';
const browserRoot = path.resolve(import.meta.dirname, '..');
const shellPath = path.join(browserRoot, 'index.html');
const bootstrapPath = path.join(browserRoot, 'src', 'browser-runtime-bootstrap.js');
const fallbackPath = path.join(browserRoot, 'src', 'browser-spatial-runtime-fallback.js');
const bootStatePath = path.join(browserRoot, 'src', 'runtime', 'boot-state.ts');
const bundledShellSourcePath = path.join(browserRoot, 'src', 'runtime', 'shell.ts');
const registryPath = path.join(browserRoot, 'src', 'runtime', 'registry.ts');
const packagePath = path.join(browserRoot, 'package.json');
const packageLockPath = path.join(browserRoot, 'package-lock.json');
const repoRoot = path.resolve(browserRoot, '..', '..');
const failures = [];
function assert(condition, message)
{
if (!condition)
{
failures.push(message);
}
}
function escapeRegExp(value)
{
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function verifyPhaseOneQueue(sourcePath)
{
if (!existsSync(sourcePath))
{
return;
}
const source = readFileSync(sourcePath, 'utf8');
const positions = [...source.matchAll(/queuePosition:\s*(\d+)/g)]
.map((match) => Number.parseInt(match[1], 10))
.sort((left, right) => left - right);
const expectedPositions = Array.from({ length: 29 }, (_, index) => index + 1);
assert(
JSON.stringify(positions) === JSON.stringify(expectedPositions),
`${path.basename(sourcePath)} must expose each Phase 1 queue position exactly once from 1 through 29.`
);
for (const row of phaseOneQueue)
{
const rowPattern = new RegExp(
`queuePosition:\\s*${row.position},[\\s\\S]{0,240}repo:\\s*'${escapeRegExp(row.repo)}'`
);
assert(
rowPattern.test(source),
`${path.basename(sourcePath)} is missing Phase 1 queue row ${row.position}: ${row.repo}.`
);
}
}
const phaseOneQueue = [
{ position: 1, repo: 'cahidenes/rubiks-cube-solver' },
{ position: 2, repo: 'tentone/rubix-solver' },
{ position: 3, repo: 'NuiLab/code-vr' },
{ position: 4, repo: 'pissang/claygl' },
{ position: 5, repo: 'pissang/clay-viewer' },
{ position: 6, repo: 'pmndrs/postprocessing' },
{ position: 7, repo: 'pmndrs/react-postprocessing' },
{ position: 8, repo: 'pmndrs/drei' },
{ position: 9, repo: 'pmndrs/uikit' },
{ position: 10, repo: 'pmndrs/three-stdlib' },
{ position: 11, repo: 'pmndrs/maath' },
{ position: 12, repo: 'pmndrs/zustand' },
{ position: 13, repo: 'pmndrs/leva' },
{ position: 14, repo: 'pmndrs/use-gesture' },
{ position: 15, repo: 'pmndrs/react-spring' },
{ position: 16, repo: 'google/model-viewer/packages/model-viewer-effects' },
{ position: 17, repo: 'google/model-viewer/packages/modelviewer.dev' },
{ position: 18, repo: 'google/model-viewer/packages/render-fidelity-tools' },
{ position: 19, repo: 'google/model-viewer/packages/space-opera' },
{ position: 20, repo: 'ecomfe/zrender' },
{ position: 21, repo: 'ecomfe/echarts-gl' },
{ position: 22, repo: 'KhronosGroup/glTF-Sample-Viewer' },
{ position: 23, repo: 'KhronosGroup/glTF-Sample-Renderer' },
{ position: 24, repo: '@react-spring/core' },
{ position: 25, repo: '@react-spring/shared' },
{ position: 26, repo: '@react-spring/types' },
{ position: 27, repo: '@react-spring/parallax' },
{ position: 28, repo: '@react-spring/rafz' },
{ position: 29, repo: '@react-spring/animated' }
];
assert(existsSync(shellPath), `Missing authoritative browser shell: ${shellPath}`);
assert(existsSync(bootstrapPath), `Missing bootstrap runtime script: ${bootstrapPath}`);
assert(existsSync(fallbackPath), `Missing clean-checkout fallback runtime: ${fallbackPath}`);
assert(existsSync(bootStatePath), `Missing browser boot-state helper: ${bootStatePath}`);
assert(existsSync(bundledShellSourcePath), `Missing bundled runtime shell source: ${bundledShellSourcePath}`);
assert(existsSync(registryPath), `Missing browser adapter registry: ${registryPath}`);
assert(existsSync(packagePath), `Missing browser package manifest: ${packagePath}`);
assert(existsSync(packageLockPath), `Missing browser package lock: ${packageLockPath}`);
verifyPhaseOneQueue(registryPath);
verifyPhaseOneQueue(fallbackPath);
const requiredExternalPaths = [
'.external/rubiks-cube-solver',
'.external/rubix-solver',
'.external/code-vr',
'.external/claygl/dist/claygl.js',
'.external/clay-viewer/dist/clay-viewer.js',
'.external/glTF-Sample-Viewer',
'.external/glTF-Sample-Renderer',
'.external/model-viewer/packages/modelviewer.dev',
'.external/model-viewer/packages/model-viewer-effects',
'.external/model-viewer/packages/render-fidelity-tools',
'.external/model-viewer/packages/space-opera',
'.external/react-spring/packages/animated',
'.external/react-spring/packages/core',
'.external/react-spring/packages/parallax',
'.external/react-spring/packages/rafz',
'.external/react-spring/packages/shared',
'.external/react-spring/packages/types'
];
for (const relativePath of requiredExternalPaths)
{
assert(
existsSync(path.join(repoRoot, relativePath)),
`Missing retained Phase 1 source or sidecar evidence: ${relativePath}`
);
}
if (existsSync(packagePath))
{
const packageManifest = JSON.parse(readFileSync(packagePath, 'utf8'));
const dependencies = packageManifest.dependencies ?? {};
const requiredDirectDependencies = [
'@google/model-viewer-effects',
'@khronosgroup/gltf-viewer',
'@pmndrs/uikit',
'@react-spring/three',
'@react-spring/web',
'@react-three/drei',
'@react-three/postprocessing',
'@use-gesture/react',
'echarts-gl',
'leva',
'maath',
'postprocessing',
'three-stdlib',
'zrender',
'zustand'
];
for (const packageName of requiredDirectDependencies)
{
assert(
typeof dependencies[packageName] === 'string',
`Missing direct browser runtime dependency: ${packageName}`
);
}
}
if (existsSync(packageLockPath))
{
const packageLock = readFileSync(packageLockPath, 'utf8');
for (const packageName of [
'@react-spring/animated',
'@react-spring/core',
'@react-spring/rafz',
'@react-spring/shared',
'@react-spring/types'
])
{
assert(
packageLock.includes(`"node_modules/${packageName}"`),
`Browser package lock is missing the retained transitive dependency ${packageName}.`
);
}
}
if (existsSync(shellPath))
{
const shellHtml = readFileSync(shellPath, 'utf8');
assert(
shellHtml.includes('./src/browser-runtime-bootstrap.js'),
'The authoritative browser shell must load the bootstrap runtime script.'
);
assert(
!shellHtml.includes('browser-spatial-runtime.ts'),
'The authoritative browser shell must not point directly at a TypeScript source entrypoint.'
);
}
if (existsSync(bundledShellSourcePath))
{
const bundledShellSource = readFileSync(bundledShellSourcePath, 'utf8');
assert(
bundledShellSource.includes('Browser Runtime Status'),
'The bundled runtime shell must expose the Browser Runtime Status surface.'
);
assert(
bundledShellSource.includes('queuedCommandCountAtRuntimeReady'),
'The bundled runtime shell must surface retained queued command count at runtime-ready.'
);
assert(
bundledShellSource.includes('lastShellStateReceivedAtUtc'),
'The bundled runtime shell must surface the latest shell-state receipt timestamp.'
);
}
if (existsSync(fallbackPath))
{
const fallbackSource = readFileSync(fallbackPath, 'utf8');
assert(
fallbackSource.includes('Browser Runtime Status'),
'The clean-checkout fallback runtime must expose the Browser Runtime Status surface.'
);
assert(
fallbackSource.includes('queuedShellStateCountAtRuntimeReady'),
'The fallback runtime must surface retained queued shell-state count at runtime-ready.'
);
assert(
fallbackSource.includes('lastCommandReceivedAtUtc'),
'The fallback runtime must surface the latest command receipt timestamp.'
);
}
if (failures.length > 0)
{
for (const failure of failures)
{
console.error(failure);
}
process.exit(1);
}
console.log('HyperTwist browser shell verification passed.');