豆豆友情提示:这是一个非官方 GitHub 代理镜像,主要用于网络测试或访问加速。请勿在此进行登录、注册或处理任何敏感信息。进行这些操作请务必访问官方网站 github.com。 Raw 内容也通过此代理提供。
Skip to content

Commit cdbc66f

Browse files
mvanhornOrKoN
andauthored
chore: add ESLint guard against direct third-party imports bypassing bundle (#1189)
## Summary - Adds a custom ESLint rule `@local/no-direct-third-party-imports` that flags value imports of bundled third-party packages (`@modelcontextprotocol/sdk`, `puppeteer-core`, `@puppeteer/browsers`, `yargs`, `debug`, `zod`, `core-js`) when used outside of `src/third_party/` - Type-only imports (`import type`) are allowed since they are erased at compile time and don't affect the bundle - The rule is scoped to `src/**/*.ts` so development scripts and tests are unaffected This prevents the class of bugs where a direct npm import works during development (devDependencies installed) but breaks in the published package (only bundled code ships). PR #1111 was an example of this exact issue caught through manual `npm pack` testing. Closes #1123 ## Test plan - [x] Verified `npx eslint --no-cache src/` passes with no violations on the current codebase - [x] Verified the rule correctly catches a test file with `import {Client} from '@modelcontextprotocol/sdk/client/index.js'` - [x] Verified the rule allows `import type {Flags} from 'lighthouse'` (type-only import) - [x] Verified the rule does not fire inside `src/third_party/index.ts` (the barrel itself) - [x] Verified scripts/ and tests/ are unaffected (rule scoped to `src/**/*.ts`) --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Alex Rudenko <alexrudenko@chromium.org>
1 parent 2e1eaba commit cdbc66f

File tree

3 files changed

+153
-0
lines changed

3 files changed

+153
-0
lines changed

eslint.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ export default defineConfig([
135135
],
136136
},
137137
},
138+
{
139+
name: 'Source files',
140+
files: ['src/**/*.ts'],
141+
rules: {
142+
'@local/no-direct-third-party-imports': 'error',
143+
},
144+
},
138145
{
139146
name: 'Tools definitions',
140147
files: ['src/tools/**/*.ts'],

scripts/eslint_rules/local-plugin.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
import checkLicenseRule from './check-license-rule.js';
88
import enforceZodSchemaRule from './enforce-zod-schema-rule.js';
9+
import noDirectThirdPartyImportsRule from './no-direct-third-party-imports-rule.js';
910

1011
export default {
1112
rules: {
1213
'check-license': checkLicenseRule,
14+
'no-direct-third-party-imports': noDirectThirdPartyImportsRule,
1315
'enforce-zod-schema': enforceZodSchemaRule,
1416
},
1517
};
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
/**
8+
* ESLint rule that prevents value (non-type) imports of third-party packages
9+
* that should go through the `src/third_party/index.ts` barrel file.
10+
*
11+
* Type-only imports are allowed because they are erased at compile time and
12+
* do not affect the bundle.
13+
*
14+
* This catches a class of bugs where a direct import works in development
15+
* (because devDependencies are installed) but fails once the package is
16+
* bundled and published via `npm pack`.
17+
*
18+
* The list of bundled packages is derived dynamically by scanning
19+
* `src/third_party/*.ts` for import/export statements at ESLint load time.
20+
*
21+
* See https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/1123
22+
*/
23+
24+
import {readdirSync, readFileSync} from 'node:fs';
25+
import {join} from 'node:path';
26+
27+
const THIRD_PARTY_DIR = join(
28+
import.meta.dirname,
29+
'..',
30+
'..',
31+
'src',
32+
'third_party',
33+
);
34+
35+
/**
36+
* Parse all .ts files in src/third_party/ and extract the bare package names
37+
* from import/export statements. Relative imports and node_modules paths
38+
* (used for chrome-devtools-frontend) are skipped.
39+
*/
40+
function discoverBundledPackages() {
41+
const packages = new Set();
42+
// Match `from 'pkg'` (may appear on a different line than `import`)
43+
// and side-effect imports like `import 'pkg'`.
44+
const fromRe = /from\s+['"]([^'"]+)['"]/g;
45+
const sideEffectRe = /^import\s+['"]([^'"]+)['"]/gm;
46+
47+
let files;
48+
try {
49+
files = readdirSync(THIRD_PARTY_DIR).filter(f => f.endsWith('.ts'));
50+
} catch {
51+
return [];
52+
}
53+
54+
for (const file of files) {
55+
const content = readFileSync(join(THIRD_PARTY_DIR, file), 'utf8');
56+
for (const re of [fromRe, sideEffectRe]) {
57+
re.lastIndex = 0;
58+
let match;
59+
while ((match = re.exec(content)) !== null) {
60+
const source = match[1];
61+
// Skip relative imports and node_modules paths.
62+
if (source.startsWith('.') || source.startsWith('/')) {
63+
continue;
64+
}
65+
// Extract the bare package name (handle scoped packages like @foo/bar).
66+
const parts = source.split('/');
67+
const pkg = source.startsWith('@')
68+
? parts.slice(0, 2).join('/')
69+
: parts[0];
70+
packages.add(pkg);
71+
}
72+
}
73+
}
74+
75+
return [...packages];
76+
}
77+
78+
const THIRD_PARTY_PACKAGES = discoverBundledPackages();
79+
80+
/** Matches any import source that starts with one of the restricted packages. */
81+
function isRestrictedSource(source) {
82+
return THIRD_PARTY_PACKAGES.some(
83+
pkg => source === pkg || source.startsWith(pkg + '/'),
84+
);
85+
}
86+
87+
/** Returns true when the file is inside src/third_party/. */
88+
function isThirdPartyBarrel(filename) {
89+
const normalized = filename.replace(/\\/g, '/');
90+
return normalized.includes('/src/third_party/');
91+
}
92+
93+
export default {
94+
name: 'no-direct-third-party-imports',
95+
meta: {
96+
type: 'problem',
97+
docs: {
98+
description:
99+
'Disallow value imports of bundled third-party packages outside of src/third_party/',
100+
},
101+
schema: [],
102+
messages: {
103+
noDirectImport:
104+
'Do not import "{{source}}" directly. Use the re-export from "src/third_party/index.js" instead so the import survives bundling. (Type-only imports are fine.)',
105+
},
106+
},
107+
defaultOptions: [],
108+
create(context) {
109+
const filename = context.filename;
110+
if (isThirdPartyBarrel(filename)) {
111+
return {};
112+
}
113+
114+
return {
115+
ImportDeclaration(node) {
116+
// `import type { Foo } from '...'` is always safe.
117+
if (node.importKind === 'type') {
118+
return;
119+
}
120+
121+
const source = node.source.value;
122+
if (!isRestrictedSource(source)) {
123+
return;
124+
}
125+
126+
// If every specifier is `type`, the import is still safe.
127+
// e.g. `import { type Foo, type Bar } from '...'`
128+
const hasValueSpecifier = node.specifiers.some(
129+
s => s.type !== 'ImportSpecifier' || s.importKind !== 'type',
130+
);
131+
132+
if (!hasValueSpecifier) {
133+
return;
134+
}
135+
136+
context.report({
137+
node,
138+
messageId: 'noDirectImport',
139+
data: {source},
140+
});
141+
},
142+
};
143+
},
144+
};

0 commit comments

Comments
 (0)