Skip to content

Commit f564063

Browse files
authored
ui: Replace plugin barrel codegen with a Vite virtual module (#5903)
The `plugin`/`core_plugin` import barrels are currently generated by a Python script (`tools/gen_ui_imports`) that write .ts and .scss files into ui/src/gen. This patch moves the .ts barrels into a Vite plugin that exposes them as virtual modules: ```ts import NON_CORE_PLUGINS from 'virtual:perfetto/all_plugins'; import CORE_PLUGINS from 'virtual:perfetto/all_core_plugins'; ``` The .scss barrel is gone too — each plugin's index.ts now does `import './styles.scss'` so Vite pulls plugin styles in via the JS module graph instead of a separate aggregated SCSS file. Drops the tools/gen_ui_imports script and its build.mjs invocations. By virtue of the added d.ts file, this makes the plugin list types available before the barrel file has been generated, getting us closer to a world where the typescript build can be checked before any artifacts are generated.
1 parent 05d93e9 commit f564063

6 files changed

Lines changed: 119 additions & 156 deletions

File tree

python/tools/check_imports.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
# [a,b] -> [c,d] is equivalent to allowing a>c, a>d, b>c, b>d.
3535
DEPS_ALLOWLIST = [
3636
# Everything can depend on base/, protos and NPM packages.
37-
('*', ['/base/*', '/protos/index', '/gen/perfetto_version', NODE_MODULES]),
37+
('*', [
38+
'/base/*', '/protos/index', '/gen/perfetto_version', NODE_MODULES,
39+
'virtual:*'
40+
]),
3841

3942
# Integration tests can depend on everything.
4043
('/test/*', '*'),

tools/gen_ui_imports

Lines changed: 0 additions & 137 deletions
This file was deleted.

ui/build.mjs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ const __dirname = path.dirname(__filename);
7777

7878
const ROOT_DIR = path.dirname(__dirname); // The repo root.
7979
const VERSION_SCRIPT = pjoin(ROOT_DIR, 'tools/write_version_header.py');
80-
const GEN_IMPORTS_SCRIPT = pjoin(ROOT_DIR, 'tools/gen_ui_imports');
8180
const DEFAULT_PORT = 10000;
8281

8382
const cfg = {
@@ -379,8 +378,6 @@ Env-var overrides:
379378

380379
buildWasm(args.no_wasm);
381380
copySyntaqliteRuntime();
382-
generateImports('ui/src/core_plugins', 'all_core_plugins');
383-
generateImports('ui/src/plugins', 'all_plugins');
384381
scanDir('ui/src/assets');
385382
scanDir('ui/src/chrome_extension');
386383
scanDir('buildtools/typefaces');
@@ -566,19 +563,6 @@ function compileProtos() {
566563
addTask(execModule, ['pbts', pbtsArgs]);
567564
}
568565

569-
function generateImports(dir, name) {
570-
// We have to use the symlink (ui/src/gen) rather than cfg.outGenDir
571-
// below since we want to generate the correct relative imports. For example:
572-
// ui/src/frontend/foo.ts
573-
// import '../gen/all_plugins.ts';
574-
// ui/src/gen/all_plugins.ts (aka ui/out/tsc/gen/all_plugins.ts)
575-
// import '../frontend/some_plugin.ts';
576-
const dstTs = pjoin(ROOT_DIR, 'ui/src/gen', name);
577-
const inputDir = pjoin(ROOT_DIR, dir);
578-
const args = [GEN_IMPORTS_SCRIPT, inputDir, '--out', dstTs];
579-
addTask(exec, ['python3', args]);
580-
}
581-
582566
// Generates a .ts source that defines the VERSION and SCM_REVISION constants.
583567
function genVersion() {
584568
const cmd = 'python3';

ui/src/frontend/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import '../base/disposable_polyfill';
1717
import '../base/static_initializers';
1818
import '../assets/perfetto.scss';
1919
import z from 'zod';
20-
import NON_CORE_PLUGINS from '../gen/all_plugins';
21-
import CORE_PLUGINS from '../gen/all_core_plugins';
20+
import NON_CORE_PLUGINS from 'virtual:perfetto/all_plugins';
21+
import CORE_PLUGINS from 'virtual:perfetto/all_core_plugins';
2222
import m from 'mithril';
2323
import {defer} from '../base/deferred';
2424
import {addErrorHandler, reportError} from '../base/logging';

ui/src/types/virtual-modules.d.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (C) 2026 The Android Open Source Project
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Type declarations for virtual modules generated by Vite plugins. See
16+
// pluginAllPluginsBarrel in ui/vite.config.mjs.
17+
//
18+
// NB: this file must be an ambient script (no top-level imports), otherwise
19+
// the `declare module` blocks would become module-scoped and tsc wouldn't
20+
// pick them up when resolving the `virtual:` imports elsewhere. Imports
21+
// inside a `declare module` block can't be relative, so we go via baseUrl.
22+
23+
declare module 'virtual:perfetto/all_plugins' {
24+
// Putting the imports here rather than hoisting to the top of the file keeps
25+
// this as an ambient script, which is required in order to get tsc to pick up
26+
// the module declarations.
27+
import type {PerfettoPluginStatic, PerfettoPlugin} from 'src/public/plugin';
28+
const plugins: PerfettoPluginStatic<PerfettoPlugin>[];
29+
export default plugins;
30+
}
31+
32+
declare module 'virtual:perfetto/all_core_plugins' {
33+
import type {PerfettoPluginStatic, PerfettoPlugin} from 'src/public/plugin';
34+
const plugins: PerfettoPluginStatic<PerfettoPlugin>[];
35+
export default plugins;
36+
}

ui/vite.config.mjs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,82 @@ function pluginEmbedMinimalSourceMap() {
103103
};
104104
}
105105

106+
// Generates barrel modules that import every plugin under ui/src/plugins (or
107+
// ui/src/core_plugins) and default-export an array of their default exports.
108+
// Replaces the on-disk barrels that tools/gen_ui_imports used to produce.
109+
//
110+
// Exposed as virtual modules so consumers do:
111+
// import NON_CORE_PLUGINS from 'virtual:perfetto/all_plugins';
112+
// import CORE_PLUGINS from 'virtual:perfetto/all_core_plugins';
113+
//
114+
// Types live in ui/src/types/virtual-modules.d.ts.
115+
function pluginAllPluginsBarrel() {
116+
const VIRTUALS = {
117+
'virtual:perfetto/all_plugins': path.join(SRC, 'plugins'),
118+
'virtual:perfetto/all_core_plugins': path.join(SRC, 'core_plugins'),
119+
};
120+
const toCamelCase = (s) => {
121+
const [first, ...rest] = s.split(/[._]/);
122+
return first +
123+
rest.map((x) => x.charAt(0).toUpperCase() + x.slice(1)).join('');
124+
};
125+
const generate = (dir) => {
126+
const entries = fs.readdirSync(dir)
127+
.map((name) => ({name, full: path.join(dir, name)}))
128+
.filter(({full}) => {
129+
try {
130+
return fs.statSync(full).isDirectory() &&
131+
fs.existsSync(path.join(full, 'index.ts'));
132+
} catch (_) { return false; }
133+
})
134+
.sort((a, b) => a.name.localeCompare(b.name));
135+
const imports = entries
136+
.map(({name, full}) => `import ${toCamelCase(name)} from '${full}';`)
137+
.join('\n');
138+
const arr = entries.map(({name}) => ` ${toCamelCase(name)},`).join('\n');
139+
return `${imports}\n\nexport default [\n${arr}\n];\n`;
140+
};
141+
let server = null;
142+
return {
143+
name: 'perfetto:all-plugins-barrel',
144+
configureServer(s) {
145+
server = s;
146+
// Watch the parent dirs so adding/removing a plugin dir invalidates
147+
// the barrel even before any file inside it changes. addWatchFile in
148+
// load() only covers index.ts files that already exist at load time.
149+
for (const dir of Object.values(VIRTUALS)) s.watcher.add(dir);
150+
},
151+
resolveId(id) {
152+
if (id in VIRTUALS) return '\0' + id;
153+
},
154+
load(id) {
155+
if (!id.startsWith('\0virtual:perfetto/')) return;
156+
const realId = id.slice(1);
157+
const dir = VIRTUALS[realId];
158+
if (!dir) return;
159+
// Tell Rollup we depend on every index.ts so edits/deletes trigger
160+
// a rebuild in `vite build --watch`.
161+
for (const name of fs.readdirSync(dir)) {
162+
const idx = path.join(dir, name, 'index.ts');
163+
if (fs.existsSync(idx)) this.addWatchFile(idx);
164+
}
165+
return generate(dir);
166+
},
167+
handleHotUpdate(ctx) {
168+
// Invalidate the matching barrel when any file under one of the
169+
// plugin parent dirs is added/changed/removed (catches new plugin
170+
// dirs being dropped in). Edits to existing plugin source don't need
171+
// to invalidate the barrel itself — Vite handles those normally.
172+
if (!server) return;
173+
for (const [realId, dir] of Object.entries(VIRTUALS)) {
174+
if (!ctx.file.startsWith(dir + path.sep)) continue;
175+
const mod = server.moduleGraph.getModuleById('\0' + realId);
176+
if (mod) server.moduleGraph.invalidateModule(mod);
177+
}
178+
},
179+
};
180+
}
181+
106182
function pluginGenRelativeImports() {
107183
return {
108184
name: 'perfetto:gen-relative-imports',
@@ -151,6 +227,7 @@ export default defineConfig({
151227
// No HTML index — build.mjs handles HTML.
152228
appType: 'custom',
153229
plugins: [
230+
pluginAllPluginsBarrel(),
154231
pluginGenRelativeImports(),
155232
...(NO_SOURCE_MAPS ? [] : [pluginEmbedMinimalSourceMap()]),
156233
],

0 commit comments

Comments
 (0)