166 lines
4.9 KiB
JavaScript
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`
|
|
)
|