#!/usr/bin/env node /** * Copyright (c) Microsoft Corporation. * * 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. */ // @ts-check /** * Validates DEPS.list against DEPS.true (generated by build_deps_true.js). * Reports declared dependencies in DEPS.list that are not actually used by * any file, according to DEPS.true. * * Usage: node utils/build_deps_true.js && node utils/validate_deps.js */ const fs = require('fs'); const path = require('path').posix; const packagesDir = path.resolve(path.join(__dirname, '..', 'packages')); const packages = new Map(); packages.set('web', packagesDir + '/web/src/'); packages.set('injected', packagesDir + '/injected/src/'); packages.set('isomorphic', packagesDir + '/isomorphic/'); packages.set('utils', packagesDir + '/utils/'); packages.set('testIsomorphic', packagesDir + '/playwright/src/isomorphic/'); let hasErrors = false; function main() { const depsListFiles = []; findFiles(packagesDir, 'DEPS.list', depsListFiles); for (const depsListPath of depsListFiles.sort()) { const depsDir = path.dirname(depsListPath); const depsTruePath = path.join(depsDir, 'DEPS.true'); // Parse DEPS.list into groups. const declared = parseDepsFile(depsListPath, depsDir); // Parse DEPS.true (actual imports). Missing file means no cross-dir deps. const actual = fs.existsSync(depsTruePath) ? parseDepsTrue(depsTruePath) : new Map(); validateDirectory(depsDir, declared, actual); } if (hasErrors) process.exit(1); } /** * Parse a DEPS.list file into a Map. * Each entry is { original, resolved } where resolved is the absolute path * (or node_modules/... or special token). */ function parseDepsFile(filePath, depsDir) { /** @type {Map>} */ const groups = new Map(); let currentGroup = '*'; groups.set('*', []); for (const line of fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean).filter(l => !l.startsWith('#'))) { const groupMatch = line.match(/^\[(.*)\]$/); if (groupMatch) { currentGroup = groupMatch[1]; if (!groups.has(currentGroup)) groups.set(currentGroup, []); continue; } let resolved; if (line === '***' || line === '**' || line === '"strict"') { resolved = line; } else if (line.startsWith('node_modules/')) { resolved = line; } else if (line.startsWith('@')) { resolved = line.replace(/@([\w-]+)\/(.*)/, (_, arg1, arg2) => { const base = packages.get(arg1); return base ? base + arg2 : '/' + arg1 + '/' + arg2; }); } else { resolved = path.resolve(depsDir, line); } groups.get(currentGroup).push({ original: line, resolved }); } return groups; } /** * Parse a DEPS.true file into a Map>. * Import paths are relative to the DEPS.true directory (same as they appear in the file). */ function parseDepsTrue(filePath) { /** @type {Map>} */ const files = new Map(); let currentFile = null; for (const line of fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean)) { const groupMatch = line.match(/^\[(.*)\]$/); if (groupMatch) { currentFile = groupMatch[1]; files.set(currentFile, new Set()); continue; } if (currentFile) files.get(currentFile).add(line); } return files; } /** * Check each entry in DEPS.list: is it matched by at least one actual import * from a file the entry governs? */ function validateDirectory(depsDir, declared, actual) { // Collect all actual import paths (resolved to absolute) for files governed by each group. // [*] group governs all files; [file.ts] governs only that file. const errors = []; for (const [group, entries] of declared) { // Skip groups with wildcard — everything is allowed. if (entries.some(e => e.resolved === '***' || e.resolved === '**')) continue; // Determine which files this group governs. /** @type {Set} absolute import paths from governed files */ const governedImports = new Set(); if (group === '*') { // All files in the directory. for (const [, imports] of actual) { for (const imp of imports) governedImports.add(imp); } } else { // Specific file. const fileImports = actual.get(group); if (fileImports) { for (const imp of fileImports) governedImports.add(imp); } } for (const entry of entries) { if (entry.resolved === '"strict"') continue; if (!isEntryUsed(entry, governedImports, depsDir)) errors.push(` Unused: '${entry.original}' in [${group}]`); } } if (errors.length) { hasErrors = true; const rel = path.relative(packagesDir, path.join(depsDir, 'DEPS.list')); console.log(rel + ':'); for (const error of errors) console.log(error); } } /** * Check if a DEPS.list entry is matched by at least one actual import. * The entry.resolved is an absolute path (possibly with ** glob suffix), * or a node_modules/ specifier. * governedImports are relative paths from DEPS.true (relative to depsDir). */ function isEntryUsed(entry, governedImports, depsDir) { const { resolved } = entry; for (const imp of governedImports) { const absImp = imp.startsWith('node_modules/') ? imp : path.resolve(depsDir, imp); if (resolved.startsWith('node_modules/')) { if (absImp === resolved || absImp.startsWith(resolved + '/')) return true; continue; } // Glob: foo/** matches anything under foo/. if (resolved.endsWith('**')) { const parent = resolved.substring(0, resolved.length - 2); if (absImp.startsWith(parent)) return true; continue; } // Directory dep (e.g., ./codegen/ or ../protocol/): matches imports whose // directory is at or under the resolved path. if (isDirectory(resolved)) { if (absImp.startsWith(resolved + '/') || absImp === resolved) return true; continue; } // Exact file match. if (absImp === resolved) return true; // Match without extension. if (absImp === resolved + '.ts' || absImp === resolved + '.tsx' || absImp === resolved + '.d.ts') return true; // The dep might point to a directory (with index.ts) that we resolved as a file. const impDir = path.dirname(absImp); if (impDir === resolved) return true; } return false; } function isDirectory(p) { return fs.existsSync(p) && fs.statSync(p).isDirectory(); } function findFiles(dir, name, result) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.resolve(dir, entry.name); if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== 'bundles') findFiles(full, name, result); else if (entry.name === name) result.push(full); } } main();