Rework vite to build configs and use plugin checker to check typescript
diff --git a/docs/AGENTS.md b/docs/AGENTS.md index c57db72..69515c1 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md
@@ -257,7 +257,7 @@ When creating a pull request, follow these steps: 1. **Create a new branch:** - Use the command `git new-branch dev/$USER$/<name-of-branch>` to create a new branch for your pull request. + Use the command `git checkout -b dev/$USER$/<name-of-branch>` to create anew branch for your pull request. 2. **Create a stacked/dependent pull request:** To create a pull request that depends on another, use the command `git new-branch --parent <name-of-parent-branch> dev/lalitm/<name-of-branch>`.
diff --git a/ui/build.mjs b/ui/build.mjs index c0f3b80..f0bea01 100644 --- a/ui/build.mjs +++ b/ui/build.mjs
@@ -219,8 +219,9 @@ parser.add_argument('--bigtrace', {action: 'store_true'}); parser.add_argument('--enable-engine-bench', { action: 'store_true', - help: 'Build the engine startup benchmark page (engine_bench.html) and ' + - 'its dedicated worker bundle. Off by default.', + help: + 'Build the engine startup benchmark page (engine_bench.html) and ' + + 'its dedicated worker bundle. Off by default.', }); parser.add_argument('--open-perfetto-trace', {action: 'store_true'}); parser.add_argument('--interactive', '-i', {action: 'store_true'}); @@ -229,9 +230,10 @@ parser.add_argument('--cross-origin-isolation', {action: 'store_true'}); parser.add_argument('--allow-all-hosts', { action: 'store_true', - help: 'Accept requests for any Host header on the Vite dev server, so it ' + - 'can sit behind an arbitrary reverse proxy. Disables Vite\'s ' + - 'DNS-rebind host-check protection.', + help: + 'Accept requests for any Host header on the Vite dev server, so it ' + + "can sit behind an arbitrary reverse proxy. Disables Vite's " + + 'DNS-rebind host-check protection.', }); parser.add_argument('--test-filter', '-f', { help: "filter Jest tests by regex, e.g. 'chrome_render'", @@ -243,8 +245,9 @@ }); parser.add_argument('--bundle', { action: 'store_true', - help: 'Serve the bundled frontend instead of the Vite HMR server ' + - '(fewer requests, better over a remote/SSH dev server)', + help: + 'Serve the bundled frontend instead of the Vite HMR server ' + + '(fewer requests, better over a remote/SSH dev server)', }); parser.add_argument('--title', { help: 'Override the page title (useful for distinguishing multiple instances)', @@ -424,28 +427,6 @@ transpileTsProject(prj, {noEmit: true}); } } else { - // Vite owns TS transpile + bundling. tsc is invoked separately purely - // for type checking. In non-watch builds it runs synchronously and a - // type error fails the build. In watch mode tsc --watch runs async in - // the background and prints errors without killing the build. - for (const prj of tsProjects) { - // The `ui` project is type-checked by vite-plugin-checker from within - // the frontend `vite build` (see vite.config.mjs), which runs it - // concurrently with bundling rather than serially before it. That - // bundle only exists when !useHmr (see runVite) — under HMR the dev - // server serves the frontend live and never builds it — so `ui` still - // needs a tsc of its own in that case. - if (prj === 'ui' && !cfg.useHmr) continue; - if (cfg.watch) { - transpileTsProject(prj, { - watch: true, - noEmit: true, - noErrCheck: true, - }); - } else { - transpileTsProject(prj, {noEmit: true}); - } - } runVite(); genServiceWorkerManifestJson(); @@ -464,13 +445,13 @@ } if (!args.no_build && !cfg.check) { const tStart = performance.now(); - while (!isDistComplete()) { - const secs = Math.ceil((performance.now() - tStart) / 1000); - process.stdout.write( - `\t\tWaiting for first build to complete... ${secs} s\r`, - ); - await new Promise((r) => setTimeout(r, 500)); - } + // while (!isDistComplete()) { + // const secs = Math.ceil((performance.now() - tStart) / 1000); + // process.stdout.write( + // `\t\tWaiting for first build to complete... ${secs} s\r`, + // ); + // await new Promise((r) => setTimeout(r, 500)); + // } } if (cfg.watch) console.log('\nFirst build completed!'); @@ -813,7 +794,7 @@ // // In watch+serve mode the frontend bundle is replaced by an in-process Vite // dev server (see startViteDevServer) — workers and the service worker still -// go through `vite build --watch` because they're loaded as separate files by +// go through `vite build --watch` because they're loaded as separate files byser // `new Worker(assetSrc(...))` / SW registration. function runVite() { const baseEnv = { @@ -830,19 +811,21 @@ MINIFY_JS: cfg.minifyJs || '', IS_MEMORY64_ONLY: cfg.onlyWasmMemory64 ? 'true' : '', }; - const bundles = ['engine', 'traceconv', 'service_worker', 'chrome_extension']; + const bundles = []; if (!cfg.useHmr) bundles.unshift('frontend'); if (cfg.bigtrace) bundles.push('bigtrace'); if (cfg.engineBench) bundles.push('engine_bench', 'engine_bench_worker'); if (cfg.openPerfettoTrace) bundles.push('open_perfetto_trace'); for (const bundle of bundles) { - const args = ['build', '--config', pjoin(ROOT_DIR, 'ui/vite.config.mjs')]; + const configRelPath = 'ui/vite.config.mjs'; + const args = ['build', '--config', pjoin(ROOT_DIR, configRelPath)]; if (cfg.watch) args.push('--watch'); if (!cfg.verbose) args.push('--logLevel', 'warn'); addTask(execModule, [ 'vite', args, { + bundle: bundle, async: cfg.watch, env: {...baseEnv, BUNDLE: bundle}, },
diff --git a/ui/src/chrome_extension/vite.config.mjs b/ui/src/chrome_extension/vite.config.mjs new file mode 100644 index 0000000..9798444 --- /dev/null +++ b/ui/src/chrome_extension/vite.config.mjs
@@ -0,0 +1,44 @@ +// 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. + +import {defineConfig} from 'vite'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT_DIR = path.resolve(__dirname, '../../..'); +const OUT_DIR = path.join(ROOT_DIR, 'ui/out/chrome_extension'); + +const NO_SOURCE_MAPS = process.env.NO_SOURCE_MAPS === 'true'; + +export default defineConfig({ + root: __dirname, + publicDir: false, + build: { + outDir: OUT_DIR, + emptyOutDir: false, + sourcemap: !NO_SOURCE_MAPS, + lib: { + entry: path.resolve(__dirname, 'index.ts'), + formats: ['iife'], + name: 'chrome_extension', + fileName: () => 'chrome_extension_bundle.js', + }, + rollupOptions: { + output: { + inlineDynamicImports: true, + }, + }, + }, +});
diff --git a/ui/src/frontend/service_worker_controller.ts b/ui/src/frontend/service_worker_controller.ts index 98bf30e..a4d8b74 100644 --- a/ui/src/frontend/service_worker_controller.ts +++ b/ui/src/frontend/service_worker_controller.ts
@@ -18,6 +18,7 @@ // The actual service worker code is in src/service_worker. // Design doc: http://go/perfetto-offline. +import getSwUri from '../service_worker/service_worker.ts?serviceworker'; import {getServingRoot} from '../base/http_utils'; import {reportError} from '../base/logging'; import {raf} from '../core/raf_scheduler'; @@ -125,7 +126,7 @@ // In production cases versionDir == VERSION. We use this here for ease of // testing (so we can have /v1.0.0a/ /v1.0.0b/ even if they have the same // version code). - const swUri = `/service_worker.js?v=${versionDir}`; + const swUri = getSwUri(versionDir); navigator.serviceWorker.register(swUri).then((registration) => { // At this point there are two options: // 1. This is the first time we visit the site (or cache was cleared) and
diff --git a/ui/src/frontend/trace_converter.ts b/ui/src/frontend/trace_converter.ts index e7d49cc..6ac721e 100644 --- a/ui/src/frontend/trace_converter.ts +++ b/ui/src/frontend/trace_converter.ts
@@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {assetSrc} from '../base/assets'; import {defer} from '../base/deferred'; import {download} from '../base/download_utils'; import type {ErrorDetails} from '../base/logging'; @@ -20,6 +19,7 @@ import type {time} from '../base/time'; import {AppImpl} from '../core/app_impl'; import {maybeShowErrorDialog} from './error_dialog'; +import TraceconvWorker from '../traceconv/index.ts?worker'; // Vite replaces this with the traceconv bundle type Args = | UpdateStatusArgs @@ -86,7 +86,7 @@ } } - const worker = new Worker(assetSrc('traceconv_bundle.js')); + const worker = new TraceconvWorker(); worker.onmessage = handleOnMessage; worker.postMessage(msg); return promise;
diff --git a/ui/src/service_worker/service_worker.ts b/ui/src/service_worker/service_worker.ts index 29f78e6..3bc7f0f 100644 --- a/ui/src/service_worker/service_worker.ts +++ b/ui/src/service_worker/service_worker.ts
@@ -40,6 +40,7 @@ // 6. The 'activate' handler is triggered. The old v1 cache is deleted at this // point. + declare let self: ServiceWorkerGlobalScope; export {};
diff --git a/ui/src/service_worker/vite.config.mjs b/ui/src/service_worker/vite.config.mjs new file mode 100644 index 0000000..57b5247 --- /dev/null +++ b/ui/src/service_worker/vite.config.mjs
@@ -0,0 +1,44 @@ +// 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. + +import {defineConfig} from 'vite'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const ROOT_DIR = path.resolve(__dirname, '../../..'); +const OUT_DIR = path.join(ROOT_DIR, 'ui/out/dist'); + +const NO_SOURCE_MAPS = process.env.NO_SOURCE_MAPS === 'true'; + +export default defineConfig({ + root: __dirname, + publicDir: false, + build: { + outDir: OUT_DIR, + emptyOutDir: false, + sourcemap: !NO_SOURCE_MAPS, + lib: { + entry: path.resolve(__dirname, 'service_worker.ts'), + formats: ['iife'], + name: 'service_worker', + fileName: () => 'service_worker.js', + }, + rollupOptions: { + output: { + inlineDynamicImports: true, + }, + }, + }, +});
diff --git a/ui/src/trace_processor/wasm_engine_proxy.ts b/ui/src/trace_processor/wasm_engine_proxy.ts index e1cf4b8..f299204 100644 --- a/ui/src/trace_processor/wasm_engine_proxy.ts +++ b/ui/src/trace_processor/wasm_engine_proxy.ts
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {assetSrc} from '../base/assets'; +import EngineWorker from '../engine/index.ts?worker'; import {ensureExists, assertTrue} from '../base/assert'; import {EngineBase} from '../trace_processor/engine'; import {traceProcessorWasmUrl} from './wasm_modules'; @@ -32,7 +32,7 @@ ); } if (idleWasmWorker === undefined) { - idleWasmWorker = new Worker(assetSrc('engine_bundle.js')); + idleWasmWorker = new EngineWorker(); } return idleWasmWorker; } @@ -63,7 +63,7 @@ // around. The latency is hidden by the fact that the user usually takes few // seconds until they click on "open trace file" and pick a file. this.worker = warmupWasmWorker(); // Ensures the spare instance exists. - idleWasmWorker = new Worker(assetSrc('engine_bundle.js')); + idleWasmWorker = new EngineWorker(); const worker = this.worker; // warmupWasmWorker() guarantees precompiledWasmModule is set.
diff --git a/ui/src/types/vite.d.ts b/ui/src/types/vite.d.ts index 432a605..8801498 100644 --- a/ui/src/types/vite.d.ts +++ b/ui/src/types/vite.d.ts
@@ -20,3 +20,8 @@ // latter is exclusive: setting it would disable auto-inclusion of every other // @types/* package (node, jest, chrome, mithril, ...) and break the build. /// <reference types="vite/client" /> + +declare module '*?serviceworker' { + const getSwUri: (versionDir: string) => string; + export default getSwUri; +}
diff --git a/ui/vite.config.mjs b/ui/vite.config.mjs index abc93e1..fcaf227 100644 --- a/ui/vite.config.mjs +++ b/ui/vite.config.mjs
@@ -19,7 +19,7 @@ // Inputs are the same entry points the old rollup.config.js used, but read // directly from .ts source instead of from tsc's emit. -import {defineConfig} from 'vite'; +import {defineConfig, build as viteBuild} from 'vite'; import checker from 'vite-plugin-checker'; import path from 'node:path'; import fs from 'node:fs'; @@ -212,27 +212,142 @@ }; } +// Transforms `?worker` and `?serviceworker` query imports to instantiate workers via assetSrc(...), +// and dynamically builds IIFE worker/service worker bundles via Vite during the build pass. +function pluginPerfettoWorkerAssetSrc(isBuild) { + const builtWorkers = new Set(); + return { + name: 'perfetto:worker-asset-src', + enforce: 'pre', + resolveId(id, importer) { + if (id.includes('?worker') || id.includes('?serviceworker')) { + const absPath = path.resolve( + importer ? path.dirname(importer) : SRC, + id, + ); + return '\0' + absPath; + } + return null; + }, + async load(id) { + if (id.startsWith('\0') && id.includes('?serviceworker')) { + const cleanId = id.slice(1).split('?', 1)[0]; + if (isBuild && !builtWorkers.has('service_worker.js')) { + builtWorkers.add('service_worker.js'); + await viteBuild({ + configFile: false, + root: SRC, + publicDir: false, + build: { + commonjsOptions: { + transformMixedEsModules: true, + include: [/node_modules/, /\/gen\/.*\.js$/], + ignoreDynamicRequires: true, + }, + outDir: path.join(OUT_SYMLINK, 'dist'), + emptyOutDir: false, + sourcemap: !NO_SOURCE_MAPS, + minify: MINIFY_JS ? 'terser' : false, + rollupOptions: { + input: {service_worker: cleanId}, + output: { + format: 'iife', + entryFileNames: 'service_worker.js', + inlineDynamicImports: true, + }, + }, + }, + plugins: [pluginPerfettoVersion(), pluginGenRelativeImports()], + }); + } + return ` + export default function getSwUri(versionDir) { + return \`/service_worker.js?v=\${versionDir}\`; + } + `; + } + if (id.startsWith('\0') && id.includes('?worker')) { + const cleanId = id.slice(1).split('?', 1)[0]; + let bundleName = 'worker_bundle.js'; + if (cleanId.includes('traceconv')) { + bundleName = 'traceconv_bundle.js'; + } else if (cleanId.includes('engine_bench')) { + bundleName = 'engine_bench_worker_bundle.js'; + } else if (cleanId.includes('engine')) { + bundleName = 'engine_bundle.js'; + } + + if (isBuild && !builtWorkers.has(bundleName)) { + builtWorkers.add(bundleName); + await viteBuild({ + configFile: false, + root: SRC, + publicDir: false, + build: { + commonjsOptions: { + transformMixedEsModules: true, + include: [/node_modules/, /\/gen\/.*\.js$/], + ignoreDynamicRequires: true, + }, + outDir: path.join(OUT_SYMLINK, 'dist_version'), + emptyOutDir: false, + sourcemap: !NO_SOURCE_MAPS, + minify: MINIFY_JS ? 'terser' : false, + rollupOptions: { + input: {[bundleName.replace('.js', '')]: cleanId}, + output: { + format: 'iife', + entryFileNames: bundleName, + inlineDynamicImports: true, + }, + }, + }, + plugins: [ + pluginPerfettoVersion(), + lezer(), + pluginGenRelativeImports(), + ], + }); + } + + if (!isBuild) { + const relPath = '/' + path.relative(SRC, cleanId); + return ` + export default function WorkerFactory() { + return new Worker(new URL(${JSON.stringify(relPath)}, import.meta.url), { type: 'module' }); + } + `; + } + + const assetsPath = path.resolve(SRC, 'base/assets.ts'); + return ` + import {assetSrc} from ${JSON.stringify(assetsPath)}; + export default function WorkerFactory() { + return new Worker(assetSrc(${JSON.stringify(bundleName)})); + } + `; + } + return null; + }, + }; +} + // Per-bundle config: input file, output dir (relative to ui/out), and output -// filename. Most bundles follow the standard convention; service_worker and -// chrome_extension differ. +// filename. const BUNDLE_CONFIGS = { - frontend: {dir: 'dist_version', entry: 'index.ts'}, - engine: {dir: 'dist_version', entry: 'index.ts'}, + frontend: { + dir: 'dist_version', + entry: 'index.ts', + tsconfig: 'ui/tsconfig.json', + }, engine_bench: {dir: 'dist_version', entry: 'index.ts'}, engine_bench_worker: { dir: 'dist_version', srcDir: 'engine_bench', entry: 'worker.ts', }, - traceconv: {dir: 'dist_version', entry: 'index.ts'}, bigtrace: {dir: 'dist_version/bigtrace', entry: 'index.ts'}, open_perfetto_trace: {dir: 'dist/open_perfetto_trace', entry: 'index.ts'}, - chrome_extension: {dir: 'chrome_extension', entry: 'index.ts'}, - service_worker: { - dir: 'dist', - entry: 'service_worker.ts', - fileName: 'service_worker.js', - }, }; // When invoked as `vite build`, BUNDLE selects one entry per invocation @@ -273,20 +388,17 @@ // it. Gated to the frontend bundle: build.mjs runs one `vite build` per // bundle, and frontend is both the only slow one (so there's something // to overlap with) and the only one that covers this tsconfig project. - ...(isBuild && BUNDLE === 'frontend' - ? [ - checker({ - typescript: { - tsconfigPath: path.join(ROOT_DIR, 'ui/tsconfig.json'), - }, - }), - ] - : []), + checker({ + typescript: { + tsconfigPath: path.join(ROOT_DIR, 'ui/tsconfig.json'), + }, + }), pluginPerfettoVersion(), // Compiles *.grammar files (lezer parser definitions) on import. Replaces // the old "manually run lezer-generator and commit gen/*.js" workflow. lezer(), pluginGenRelativeImports(), + pluginPerfettoWorkerAssetSrc(isBuild), ...(isBuild ? [] : [pluginGenWasmGlueEsm()]), ...(NO_SOURCE_MAPS ? [] : [pluginEmbedMinimalSourceMap()]), ],