orca/config/scripts/project-renderer-web-client...

166 lines
4.9 KiB
JavaScript

import {
cpSync,
mkdirSync,
readFileSync,
readdirSync,
renameSync,
rmSync,
statSync,
writeFileSync
} from 'node:fs'
import { dirname, join, posix, resolve, sep } from 'node:path'
import { transform } from 'esbuild'
const rendererOutput = resolve('out/renderer')
const webOutput = resolve('out/web')
const stagingOutput = resolve(dirname(webOutput), `.web-projection-${process.pid}`)
const manifestPath = join(rendererOutput, '.vite', 'manifest.json')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
const selectedFiles = new Set(['web-index.html'])
const visitedEntries = new Set()
function assertEntryIsolation() {
const entryKeys = new Set(
Object.entries(manifest)
.filter(([, entry]) => entry?.isEntry === true)
.map(([key]) => key)
)
for (const sourceEntry of entryKeys) {
const visited = new Set()
const pending = [sourceEntry]
while (pending.length > 0) {
const key = pending.pop()
if (visited.has(key)) {
continue
}
visited.add(key)
const entry = manifest[key]
if (!entry || typeof entry !== 'object') {
throw new Error(`Renderer manifest is missing entry: ${key}`)
}
for (const dependency of [...(entry.imports ?? []), ...(entry.dynamicImports ?? [])]) {
if (entryKeys.has(dependency) && dependency !== sourceEntry) {
throw new Error(`Renderer entry ${sourceEntry} executes entry ${dependency}`)
}
pending.push(dependency)
}
}
}
}
function addOutputPath(outputPath) {
if (
typeof outputPath !== 'string' ||
outputPath.length === 0 ||
outputPath.startsWith('/') ||
/^[A-Za-z]:/.test(outputPath) ||
outputPath.includes('\\') ||
outputPath.split('/').includes('..')
) {
throw new Error(`Invalid renderer output path: ${String(outputPath)}`)
}
selectedFiles.add(outputPath)
}
function visitManifestEntry(key) {
if (visitedEntries.has(key)) {
return
}
visitedEntries.add(key)
const entry = manifest[key]
if (!entry || typeof entry !== 'object') {
throw new Error(`Renderer manifest is missing entry: ${key}`)
}
addOutputPath(entry.file)
for (const outputPath of [...(entry.css ?? []), ...(entry.assets ?? [])]) {
addOutputPath(outputPath)
}
for (const dependency of [...(entry.imports ?? []), ...(entry.dynamicImports ?? [])]) {
visitManifestEntry(dependency)
}
}
function listOutputFiles(directory, prefix = '') {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const outputPath = prefix ? join(prefix, entry.name) : entry.name
return entry.isDirectory()
? listOutputFiles(join(directory, entry.name), outputPath)
: [outputPath.split(sep).join('/')]
})
}
function includeReferencedOutputs() {
const candidates = listOutputFiles(rendererOutput).filter(
(outputPath) => !outputPath.startsWith('.vite/') && !outputPath.endsWith('.html')
)
let foundReference = true
while (foundReference) {
foundReference = false
for (const selectedFile of selectedFiles) {
if (!/\.(?:css|html|m?js|svg)$/.test(selectedFile)) {
continue
}
const contents = readFileSync(join(rendererOutput, selectedFile), 'utf8')
for (const candidate of candidates) {
if (selectedFiles.has(candidate)) {
continue
}
const localReference = posix.relative(posix.dirname(selectedFile), candidate)
if (contents.includes(candidate) || contents.includes(localReference)) {
selectedFiles.add(candidate)
foundReference = true
}
}
}
}
}
async function minifyWebOutput() {
await Promise.all(
[...selectedFiles]
.filter((outputPath) => /\.(?:css|m?js)$/.test(outputPath))
.map(async (outputPath) => {
const targetPath = join(stagingOutput, outputPath)
const loader = outputPath.endsWith('.css') ? 'css' : 'js'
const result = await transform(readFileSync(targetPath, 'utf8'), {
legalComments: 'none',
loader,
minify: true,
target: 'es2020'
})
writeFileSync(targetPath, result.code)
})
)
}
assertEntryIsolation()
visitManifestEntry('web-index.html')
includeReferencedOutputs()
rmSync(stagingOutput, { force: true, recursive: true })
try {
for (const outputPath of selectedFiles) {
const targetPath = join(stagingOutput, outputPath)
mkdirSync(dirname(targetPath), { recursive: true })
cpSync(join(rendererOutput, outputPath), targetPath)
}
await minifyWebOutput()
rmSync(webOutput, { force: true, recursive: true })
renameSync(stagingOutput, webOutput)
} finally {
rmSync(stagingOutput, { force: true, recursive: true })
}
const outputBytes = [...selectedFiles].reduce(
(total, outputPath) => total + statSync(join(webOutput, outputPath)).size,
0
)
console.log(
`Projected web client: ${selectedFiles.size} files, ${(outputBytes / 1024 / 1024).toFixed(1)} MiB`
)