blob: 0b2f2bc021730172a453ef41aa246463e5d51723 [file] [edit]
// Copyright (C) 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Vite orchestration: production bundle builds and in-process dev server.
import fs from 'node:fs';
import path from 'node:path';
import * as vite from 'vite';
import {runInProcStep} from './steps.mjs';
const pjoin = path.join;
export const ALL_BUNDLES = [
'frontend',
'engine',
'traceconv',
'service_worker',
'chrome_extension',
];
// Worker bundles need a real .js file on disk even in dev — they're loaded
// via `new Worker(assetSrc(...))`, which the Vite dev server's module graph
// doesn't intercept.
export const WORKER_BUNDLES = ['engine', 'traceconv'];
// Optional bundles, only built when the corresponding flag is passed.
export const OPEN_PERFETTO_TRACE_BUNDLE = 'open_perfetto_trace';
// Runs `vite build` in-process for each named bundle. The config file
// (ui/vite.config.mjs) selects its input from process.env.BUNDLE, so we set
// that before each call. Bundles are built sequentially: vite-plugin-checker
// runs tsc in the frontend bundle and we don't want N copies of it racing.
export async function viteBuild({rootDir, bundles, mode}) {
const configFile = pjoin(rootDir, 'ui/vite.config.mjs');
// Vite (and any tooling it spawns, e.g. vite-plugin-checker's tsc) expects
// the cwd to be ui/ so it picks up ui/tsconfig.json and resolves
// node_modules naturally. Restore the previous cwd when done.
const prevCwd = process.cwd();
process.chdir(pjoin(rootDir, 'ui'));
try {
for (const bundle of bundles) {
process.env.BUNDLE = bundle;
await runInProcStep(`vite build (${bundle})`, () =>
vite.build({configFile, mode, logLevel: 'warn'}),
);
}
} finally {
process.chdir(prevCwd);
}
}
// Starts the Vite dev server in-process. Vite owns the user-facing port,
// serves frontend/index.ts as native ESM transformed on the fly, and runs
// HMR. We layer a few middlewares on top:
// - /test/* serves files from the repo (used by some e2e flows).
// - /frontend.css returns an empty 200 because frontend/index.ts inserts
// a <link rel=stylesheet> for it at runtime (needed in prod); in dev the
// styles come from the SCSS module that Vite transforms inline.
// - Anything Vite doesn't claim falls back to outDir/dist/v<version>/
// (wasm modules, fonts under /assets/, etc.).
// - / and /index.html serve the patched ui/src/assets/index.html via
// server.transformIndexHtml so pluginPatchIndexHtml fires.
export async function viteDev({
rootDir,
outDir,
version,
host = '127.0.0.1',
port,
crossOriginIsolation = false,
}) {
// Static files (wasm, fonts, etc.) live under dist/v<version>/. We serve
// them at root-relative URLs in dev because the patched index.html sets
// version='.' (see pluginPatchIndexHtml).
const distRootDir = pjoin(outDir, 'dist', version);
const indexSrc = pjoin(rootDir, 'ui/src/assets/index.html');
const prevCwd = process.cwd();
process.chdir(pjoin(rootDir, 'ui'));
let server;
try {
const headers = crossOriginIsolation
? {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
}
: undefined;
server = await vite.createServer({
configFile: pjoin(rootDir, 'ui/vite.config.mjs'),
server: {
host,
port,
strictPort: false,
headers,
fs: {allow: [rootDir]},
},
});
} finally {
process.chdir(prevCwd);
}
server.middlewares.use((req, res, next) => {
const url = req.url.split('?', 1)[0];
if (!url.startsWith('/test/')) return next();
const absPath = pjoin(rootDir, url);
if (path.relative(rootDir, absPath).startsWith('..')) {
res.statusCode = 403;
return res.end('403');
}
fs.readFile(absPath, (err, data) => {
if (err) {
res.statusCode = 404;
return res.end();
}
res.end(data);
});
});
server.middlewares.use((req, res, next) => {
const url = req.url.split('?', 1)[0];
if (url === '/frontend.css' || url.endsWith('/frontend.css')) {
res.setHeader('Content-Type', 'text/css');
return res.end('/* dev stub: styles injected by Vite */');
}
next();
});
server.middlewares.use((req, res, next) => {
const url = req.url.split('?', 1)[0];
if (url === '/' || url === '/index.html') return next();
const absPath = path.normalize(pjoin(distRootDir, url));
if (path.relative(distRootDir, absPath).startsWith('..')) return next();
fs.stat(absPath, (err, stat) => {
if (err || !stat.isFile()) return next();
fs.readFile(absPath, (rerr, data) => {
if (rerr) return next();
const ext = url.split('.').pop();
const mime =
{
wasm: 'application/wasm',
woff2: 'font/woff2',
png: 'image/png',
json: 'application/json',
css: 'text/css',
js: 'application/javascript',
html: 'text/html',
}[ext] || 'application/octet-stream';
res.setHeader('Content-Type', mime);
res.end(data);
});
});
});
server.middlewares.use(async (req, res, next) => {
const url = req.url.split('?', 1)[0];
if (url !== '/' && url !== '/index.html') return next();
try {
const raw = fs.readFileSync(indexSrc, 'utf8');
const transformed = await server.transformIndexHtml(url, raw);
res.setHeader('Content-Type', 'text/html');
res.end(transformed);
} catch (e) {
next(e);
}
});
await server.listen();
server.printUrls();
process.on('SIGINT', () => server.close().then(() => process.exit(0)));
process.on('SIGTERM', () => server.close().then(() => process.exit(0)));
}