Skip to content

Commit ab0ecc4

Browse files
authored
chore: introduce check-deps (#864)
1 parent f010164 commit ab0ecc4

File tree

7 files changed

+202
-1
lines changed

7 files changed

+202
-1
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
"license": "Apache-2.0",
1818
"scripts": {
1919
"build": "tsc",
20-
"lint": "npm run update-readme && eslint . && tsc --noEmit",
20+
"lint": "npm run update-readme && npm run check-deps && eslint . && tsc --noEmit",
2121
"lint-fix": "eslint . --fix",
22+
"check-deps": "node utils/check-deps.js",
2223
"update-readme": "node utils/update-readme.js",
2324
"watch": "tsc --watch",
2425
"test": "playwright test",

src/DEPS.list

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[*]
2+
./tools/
3+
./mcp/
4+
5+
[program.ts]
6+
***

src/extension/DEPS.list

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[*]
2+
../
3+
../mcp/

src/loopTools/DEPS.list

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[*]
2+
../
3+
../loop/
4+
../mcp/

src/mcp/DEPS.list

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[*]
2+
../log.js
3+
../manualPromise.js
4+
../httpServer.js

src/tools/DEPS.list

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[*]
2+
../javascript.js
3+
../log.js
4+
../manualPromise.js

utils/check-deps.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Copyright 2019 Google Inc. All rights reserved.
4+
* Modifications copyright (c) Microsoft Corporation.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
// @ts-check
20+
21+
import fs from 'fs';
22+
import ts from 'typescript';
23+
import path from 'path';
24+
25+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
26+
27+
const depsCache = {};
28+
const packageRoot = path.resolve(__dirname, '..');
29+
30+
async function checkDeps() {
31+
const deps = new Set();
32+
const src = path.join(packageRoot, 'src');
33+
34+
const program = ts.createProgram({
35+
options: {
36+
allowJs: true,
37+
target: ts.ScriptTarget.ESNext,
38+
strict: true,
39+
},
40+
rootNames: listAllFiles(src),
41+
});
42+
const sourceFiles = program.getSourceFiles();
43+
const errors = [];
44+
sourceFiles.filter(x => !x.fileName.includes(path.sep + 'node_modules' + path.sep) && !x.fileName.includes(path.sep + 'bundles' + path.sep)).map(x => visit(x, x.fileName, x.getFullText()));
45+
46+
if (errors.length) {
47+
for (const error of errors)
48+
console.log(error);
49+
console.log(`--------------------------------------------------------`);
50+
console.log(`Changing the project structure or adding new components?`);
51+
console.log(`Update DEPS in ${packageRoot}`);
52+
console.log(`--------------------------------------------------------`);
53+
process.exit(1);
54+
}
55+
56+
function visit(node, fileName, text) {
57+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
58+
if (node.importClause) {
59+
if (node.importClause.isTypeOnly)
60+
return;
61+
if (node.importClause.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
62+
if (node.importClause.namedBindings.elements.every(e => e.isTypeOnly))
63+
return;
64+
}
65+
}
66+
const importName = node.moduleSpecifier.text;
67+
let importPath;
68+
if (importName.startsWith('.'))
69+
importPath = path.resolve(path.dirname(fileName), importName);
70+
71+
const mergedDeps = calculateDeps(fileName);
72+
if (mergedDeps.includes('***'))
73+
return;
74+
if (importPath) {
75+
if (!fs.existsSync(importPath)) {
76+
if (fs.existsSync(importPath + '.ts'))
77+
importPath = importPath + '.ts';
78+
else if (fs.existsSync(importPath + '.tsx'))
79+
importPath = importPath + '.tsx';
80+
else if (fs.existsSync(importPath + '.d.ts'))
81+
importPath = importPath + '.d.ts';
82+
}
83+
84+
if (!allowImport(fileName, importPath, mergedDeps))
85+
errors.push(`Disallowed import ${path.relative(packageRoot, importPath)} in ${path.relative(packageRoot, fileName)}`);
86+
return;
87+
}
88+
89+
const fullStart = node.getFullStart();
90+
const commentRanges = ts.getLeadingCommentRanges(text, fullStart);
91+
for (const range of commentRanges || []) {
92+
const comment = text.substring(range.pos, range.end);
93+
if (comment.includes('@no-check-deps'))
94+
return;
95+
}
96+
97+
if (importName.startsWith('@'))
98+
deps.add(importName.split('/').slice(0, 2).join('/'));
99+
else
100+
deps.add(importName.split('/')[0]);
101+
}
102+
ts.forEachChild(node, x => visit(x, fileName, text));
103+
}
104+
105+
function calculateDeps(from) {
106+
const fromDirectory = path.dirname(from);
107+
let depsDirectory = fromDirectory;
108+
while (depsDirectory.startsWith(packageRoot) && !depsCache[depsDirectory] && !fs.existsSync(path.join(depsDirectory, 'DEPS.list')))
109+
depsDirectory = path.dirname(depsDirectory);
110+
if (!depsDirectory.startsWith(packageRoot))
111+
return [];
112+
113+
let deps = depsCache[depsDirectory];
114+
if (!deps) {
115+
const depsListFile = path.join(depsDirectory, 'DEPS.list');
116+
deps = {};
117+
let group = [];
118+
for (const line of fs.readFileSync(depsListFile, 'utf-8').split('\n').filter(Boolean).filter(l => !l.startsWith('#'))) {
119+
const groupMatch = line.match(/\[(.*)\]/);
120+
if (groupMatch) {
121+
group = [];
122+
deps[groupMatch[1]] = group;
123+
continue;
124+
}
125+
if (line === '***')
126+
group.push('***');
127+
else
128+
group.push(path.resolve(depsDirectory, line));
129+
}
130+
depsCache[depsDirectory] = deps;
131+
}
132+
133+
return [...(deps['*'] || []), ...(deps[path.relative(depsDirectory, from)] || [])]
134+
}
135+
136+
function allowImport(from, to, mergedDeps) {
137+
const fromDirectory = path.dirname(from);
138+
const toDirectory = isDirectory(to) ? to : path.dirname(to);
139+
if (to === toDirectory)
140+
to = path.join(to, 'index.ts');
141+
if (fromDirectory === toDirectory)
142+
return true;
143+
144+
for (const dep of mergedDeps) {
145+
if (dep === '***')
146+
return true;
147+
if (to === dep || toDirectory === dep)
148+
return true;
149+
if (dep.endsWith('**')) {
150+
const parent = dep.substring(0, dep.length - 2);
151+
if (to.startsWith(parent))
152+
return true;
153+
}
154+
}
155+
return false;
156+
}
157+
}
158+
159+
function listAllFiles(dir) {
160+
const dirs = fs.readdirSync(dir, { withFileTypes: true });
161+
const result = [];
162+
dirs.forEach(d => {
163+
const res = path.resolve(dir, d.name);
164+
if (d.isDirectory())
165+
result.push(...listAllFiles(res));
166+
else
167+
result.push(res);
168+
});
169+
return result;
170+
}
171+
172+
checkDeps().catch(e => {
173+
console.error(e && e.stack ? e.stack : e);
174+
process.exit(1);
175+
});
176+
177+
function isDirectory(dir) {
178+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory();
179+
}

0 commit comments

Comments
 (0)