108 lines
3.3 KiB
JavaScript
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,
|
|
};
|