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`)
}