Files
metabase/frontend/lint/side-effect-registry.js
2026-10-05 22:41:31 +13:00

108 lines
3.3 KiB
JavaScript

// Reads frontend/lint/side-effect-files.json, the registry of source files that run code at import time.
// The file list is generated by `bun frontend/lint/scripts/side-effect-files.js --update`, and `packages` is written by hand.
const fs = require("fs");
const path = require("path");
const micromatch = require("micromatch");
const DEFAULT_REGISTRY_PATH = path.resolve(__dirname, "side-effect-files.json");
// self: only the file itself relies on its side effects
// global: other code relies on its side effects
// entry: the file is a bundle entry point, so its side effects are always included
const CLASSIFICATIONS = ["self", "global", "entry"];
function loadRegistry(registryPath = DEFAULT_REGISTRY_PATH) {
const registry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
const files = registry.files || {};
return {
facades: registry.facades || [],
files,
packages: registry.packages || {},
patterns: Object.keys(files).filter((key) => key.includes("*")),
unclassified: new Set(registry.unclassified || []),
};
}
const patternMatchers = new WeakMap();
function matchingPatterns(registry, file) {
let matchers = patternMatchers.get(registry);
if (!matchers) {
matchers = registry.patterns.map((pattern) => ({
pattern,
isMatch: micromatch.matcher(pattern),
}));
patternMatchers.set(registry, matchers);
}
return matchers
.filter(({ isMatch }) => isMatch(file))
.map(({ pattern }) => pattern);
}
// An unclassified file counts as "global", and an exact key wins over a pattern.
function classify(registry, file) {
if (file in registry.files) {
return registry.files[file];
}
if (registry.unclassified.has(file)) {
return "global";
}
const [pattern] = matchingPatterns(registry, file);
return pattern == null ? null : registry.files[pattern];
}
function isFacade(registry, file) {
return registry.facades.some((prefix) => file.startsWith(prefix));
}
function classifyPackage(registry, source) {
const listed = Object.keys(registry.packages).find(
(specifier) => source === specifier || source.startsWith(`${specifier}/`),
);
return listed == null ? null : registry.packages[listed];
}
function validateRegistry(registry, effectFiles) {
const problems = [];
for (const [key, value] of Object.entries(registry.files)) {
if (!CLASSIFICATIONS.includes(value)) {
problems.push(`${key}: unknown classification "${value}"`);
}
if (registry.unclassified.has(key)) {
problems.push(`${key}: both classified and unclassified`);
}
}
for (const [specifier, value] of Object.entries(registry.packages)) {
if (specifier === "" || specifier.startsWith(".")) {
problems.push(`packages: "${specifier}" is not a bare specifier`);
}
if (value !== "global") {
problems.push(`${specifier}: unknown package classification "${value}"`);
}
}
for (const file of effectFiles) {
const values = new Set(
matchingPatterns(registry, file).map(
(pattern) => registry.files[pattern],
),
);
if (!(file in registry.files) && values.size > 1) {
problems.push(
`${file}: matches patterns with different classifications, add an exact entry`,
);
}
}
return problems;
}
module.exports = {
DEFAULT_REGISTRY_PATH,
CLASSIFICATIONS,
loadRegistry,
classify,
classifyPackage,
isFacade,
validateRegistry,
};