238 lines
8.1 KiB
JavaScript
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.');
|