Spaces:
Running
Running
File size: 10,033 Bytes
31c7d49 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | import { spawnSync } from 'node:child_process'
import { cp, mkdir, mkdtemp, readdir, readFile, rm, stat } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const viteFixtureRoot = join(repositoryRoot, 'tests/consumers/vite')
const nextClientFixtureRoot = join(repositoryRoot, 'tests/consumers/next-client')
const importMapFixtureRoot = join(repositoryRoot, 'tests/consumers/import-map')
const temporaryRoot = await mkdtemp(join(tmpdir(), 'triposplat-vite-consumer-'))
const packRoot = join(temporaryRoot, 'packs')
const consumerRoot = join(temporaryRoot, 'consumer')
const importMapRoot = join(temporaryRoot, 'import-map')
const keepTemporary = process.env.KEEP_PACKAGE_CONSUMER_TEMP === '1'
let failed = false
function executable(name) {
return process.platform === 'win32' ? `${name}.cmd` : name
}
function run(command, args, cwd = repositoryRoot) {
process.stdout.write(`\n> ${command} ${args.join(' ')}\n`)
const result = spawnSync(executable(command), args, {
cwd,
env: process.env,
stdio: 'inherit',
})
if (result.error) throw result.error
if (result.status !== 0) {
throw new Error(`${command} ${args.join(' ')} exited with status ${result.status}.`)
}
}
async function archiveContaining(fragment) {
const names = await readdir(packRoot)
const matches = names.filter((name) => name.includes(fragment) && name.endsWith('.tgz'))
if (matches.length !== 1) {
throw new Error(`Expected one '${fragment}' tarball in ${packRoot}; found ${matches.length}.`)
}
return join(packRoot, matches[0])
}
async function copyFixture(sourceRoot, destinationRoot) {
await cp(sourceRoot, destinationRoot, {
recursive: true,
filter(source) {
const name = basename(source)
return name !== 'node_modules'
&& name !== 'dist'
&& name !== '.next'
&& name !== 'package-lock.json'
},
})
}
async function filesBelow(root) {
const output = []
for (const entry of await readdir(root, { withFileTypes: true })) {
const path = join(root, entry.name)
if (entry.isDirectory()) output.push(...await filesBelow(path))
else output.push(path)
}
return output
}
async function validateBundle() {
const distRoot = join(consumerRoot, 'dist')
const files = await filesBelow(distRoot)
if (!files.some((file) => file.endsWith('.wasm'))) {
throw new Error('The installed-package Vite build omitted the ONNX Runtime WASM asset.')
}
let containsWorker = false
for (const file of files.filter((path) => path.endsWith('.js'))) {
const details = await stat(file)
if (details.size > 2_000_000) continue
const source = await readFile(file, 'utf8')
if (source.includes('triposplat-onnx-webgpu') || source.includes('TripoSplat runtime worker')) {
containsWorker = true
break
}
}
if (!containsWorker) {
throw new Error('The installed-package Vite build omitted the TripoSplat module worker.')
}
const relativeFiles = files.map((file) => file.slice(distRoot.length + 1)).sort()
process.stdout.write(`\nFresh Vite consumer build passed in ${consumerRoot}.\n`)
process.stdout.write(`Built assets:\n${relativeFiles.map((file) => ` ${file}`).join('\n')}\n`)
}
function assertInside(root, path, label) {
const pathFromRoot = relative(root, path)
if (pathFromRoot === '' || (!pathFromRoot.startsWith('..') && !isAbsolute(pathFromRoot))) return
throw new Error(`${label} resolves outside the static consumer root: ${path}`)
}
function resolveImportSpecifier(specifier, parentFile, imports) {
let target
if (specifier.startsWith('./') || specifier.startsWith('../')) {
target = resolve(dirname(parentFile), specifier)
} else {
const exact = imports[specifier]
if (typeof exact === 'string') {
target = resolve(importMapRoot, exact)
} else {
const prefix = Object.keys(imports)
.filter((key) => key.endsWith('/') && specifier.startsWith(key))
.sort((left, right) => right.length - left.length)[0]
const mappedPrefix = prefix === undefined ? undefined : imports[prefix]
if (typeof prefix !== 'string' || typeof mappedPrefix !== 'string') {
throw new Error(`No import-map entry resolves '${specifier}' imported by ${parentFile}.`)
}
target = resolve(importMapRoot, mappedPrefix, specifier.slice(prefix.length))
}
}
assertInside(importMapRoot, target, `Module '${specifier}'`)
return target
}
function staticModuleSpecifiers(source) {
const specifiers = []
const pattern = /\b(?:import|export)\s+(?:(?:[^"'();]*?)\s+from\s+)?["']([^"']+)["']/g
for (const match of source.matchAll(pattern)) specifiers.push(match[1])
return specifiers
}
async function validateMappedPackageExports(packageName, packageRoot, imports) {
const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'))
for (const [subpath, descriptor] of Object.entries(manifest.exports ?? {})) {
const importTarget = typeof descriptor === 'string' ? descriptor : descriptor?.import
if (typeof importTarget !== 'string') continue
const specifier = subpath === '.' ? packageName : `${packageName}${subpath.slice(1)}`
const mapped = imports[specifier]
if (typeof mapped !== 'string') {
throw new Error(`The native ESM fixture does not map public export '${specifier}'.`)
}
const expected = resolve(packageRoot, importTarget)
const actual = resolve(importMapRoot, mapped)
if (actual !== expected) {
throw new Error(`Import-map target for '${specifier}' is '${actual}', expected '${expected}'.`)
}
await stat(actual)
}
}
async function prepareImportMapConsumer() {
await copyFixture(importMapFixtureRoot, importMapRoot)
const scopeRoot = join(importMapRoot, 'vendor/@ai3d')
await mkdir(scopeRoot, { recursive: true })
await cp(
join(consumerRoot, 'node_modules/@ai3d/gaussian-scene'),
join(scopeRoot, 'gaussian-scene'),
{ recursive: true },
)
await cp(
join(consumerRoot, 'node_modules/@ai3d/triposplat-webgpu'),
join(scopeRoot, 'triposplat-webgpu'),
{ recursive: true },
)
}
async function validateImportMapConsumer() {
const htmlPath = join(importMapRoot, 'index.html')
const html = await readFile(htmlPath, 'utf8')
const importMapMatch = /<script\s+type=["']importmap["'][^>]*>([\s\S]*?)<\/script>/i.exec(html)
const moduleMatch = /<script\s+type=["']module["']\s+src=["']([^"']+)["'][^>]*><\/script>/i.exec(html)
if (!importMapMatch || !moduleMatch) {
throw new Error('Native ESM fixture needs an import map followed by an external module script.')
}
if ((importMapMatch.index ?? 0) > (moduleMatch.index ?? 0)) {
throw new Error('The import map must precede the module script in the HTML document.')
}
const parsed = JSON.parse(importMapMatch[1])
const imports = parsed.imports
if (!imports || typeof imports !== 'object' || Array.isArray(imports)) {
throw new Error('Native ESM fixture has no valid import-map imports object.')
}
const triposplatRoot = join(importMapRoot, 'vendor/@ai3d/triposplat-webgpu')
const gaussianSceneRoot = join(importMapRoot, 'vendor/@ai3d/gaussian-scene')
await validateMappedPackageExports('@ai3d/triposplat-webgpu', triposplatRoot, imports)
await validateMappedPackageExports('@ai3d/gaussian-scene', gaussianSceneRoot, imports)
const requiredAssets = [
join(triposplatRoot, 'dist/worker.js'),
join(triposplatRoot, 'dist/ort/ort-wasm-simd-threaded.asyncify.mjs'),
join(triposplatRoot, 'dist/ort/ort-wasm-simd-threaded.asyncify.wasm'),
]
for (const asset of requiredAssets) await stat(asset)
const entry = resolve(importMapRoot, moduleMatch[1])
assertInside(importMapRoot, entry, 'Module entry')
const pending = [entry]
const visited = new Set()
while (pending.length > 0) {
const modulePath = pending.pop()
if (visited.has(modulePath)) continue
visited.add(modulePath)
const source = await readFile(modulePath, 'utf8')
for (const specifier of staticModuleSpecifiers(source)) {
const dependency = resolveImportSpecifier(specifier, modulePath, imports)
await stat(dependency)
if (dependency.endsWith('.js') || dependency.endsWith('.mjs')) pending.push(dependency)
}
}
process.stdout.write(`\nPacked native ESM/import-map graph passed (${visited.size} JavaScript modules).\n`)
process.stdout.write('This build-time check does not launch a browser or run WebGPU inference.\n')
}
try {
await mkdir(packRoot, { recursive: true })
run('pnpm', ['--filter', '@ai3d/gaussian-scene', 'pack', '--pack-destination', packRoot])
run('pnpm', ['--filter', '@ai3d/triposplat-webgpu', 'pack', '--pack-destination', packRoot])
const sceneArchive = await archiveContaining('gaussian-scene')
const coreArchive = await archiveContaining('triposplat-webgpu')
await copyFixture(viteFixtureRoot, consumerRoot)
await copyFixture(nextClientFixtureRoot, join(consumerRoot, 'next-client'))
run('npm', [
'install',
'--ignore-scripts',
'--no-audit',
'--no-fund',
'--no-package-lock',
sceneArchive,
coreArchive,
], consumerRoot)
run('npm', ['run', 'build'], consumerRoot)
await validateBundle()
run('npm', ['run', 'typecheck:next-client'], consumerRoot)
process.stdout.write('\nPacked Next-style client component typecheck passed.\n')
process.stdout.write('This check does not install or invoke Next.js/Turbopack.\n')
await prepareImportMapConsumer()
await validateImportMapConsumer()
} catch (error) {
failed = true
process.stderr.write(`\nPacked package consumer smoke failed. Temporary files: ${temporaryRoot}\n`)
throw error
} finally {
if (!keepTemporary && !failed) await rm(temporaryRoot, { recursive: true, force: true })
else process.stdout.write(`Temporary files retained at ${temporaryRoot}\n`)
}
|