blob: 9d001e68af61729bc24f710db9de149e6bcca555 [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.
import path from 'node:path';
import fs from 'node:fs';
import {makeSynthModulePlugin} from './utils.mjs';
// Synthesises a barrel virtual module that imports every sub-directory under
// each |source| dir and exposes them as named arrays. Each source provides:
// - exportName: name of the exported array
// - dir: absolute directory to scan
// - prefix: prefix added to each entry's local binding name
// |virtualModule| is the absolute path (no extension) the barrel resolves to.
export function pluginPerfettoPluginBarrels({sources, virtualModule}) {
const pluginDirs = sources.map((s) => s.dir);
const toCamelCase = (s) => {
const [first, ...rest] = s.split(/[._]/);
return (
first + rest.map((x) => x.charAt(0).toUpperCase() + x.slice(1)).join('')
);
};
const listEntries = (dir) =>
fs
.readdirSync(dir)
.map((name) => ({name, full: path.join(dir, name)}))
.filter(({full}) => {
try {
return (
fs.statSync(full).isDirectory() &&
fs.existsSync(path.join(full, 'index.ts'))
);
} catch (_) {
return false;
}
})
.sort((a, b) => a.name.localeCompare(b.name));
const generate = (ctx) => {
const importLines = [];
const exportLines = [];
for (const {exportName, dir, prefix} of sources) {
const entries = listEntries(dir);
for (const {full} of entries) {
ctx.addWatchFile(path.join(full, 'index.ts'));
}
for (const {name, full} of entries) {
importLines.push(
`import ${toCamelCase(prefix + name)} from '${full}';`,
);
}
const arr = entries
.map(({name}) => ` ${toCamelCase(prefix + name)},`)
.join('\n');
exportLines.push(`export const ${exportName} = [\n${arr}\n];`);
}
return `${importLines.join('\n')}\n\n${exportLines.join('\n\n')}\n`;
};
const base = makeSynthModulePlugin({
name: 'perfetto:plugin-barrels',
modules: {[virtualModule]: generate},
});
let server = null;
return {
...base,
configureServer(s) {
server = s;
// Watch the parent dirs so adding/removing a plugin dir invalidates
// the barrel even before any file inside it changes.
for (const dir of pluginDirs) s.watcher.add(dir);
},
handleHotUpdate(ctx) {
if (!server) return;
for (const dir of pluginDirs) {
if (!ctx.file.startsWith(dir + path.sep)) continue;
const id = '\0perfetto:plugin-barrels:' + virtualModule;
const mod = server.moduleGraph.getModuleById(id);
if (mod) server.moduleGraph.invalidateModule(mod);
return;
}
},
};
}