Skip to content

Commit 4dacf02

Browse files
committed
fix: add ESLint guard against direct third-party imports bypassing bundle
Adds a custom ESLint rule `@local/no-direct-third-party-imports` that prevents value imports of bundled third-party packages from files in `src/` unless they go through the `src/third_party/index.ts` barrel. Type-only imports are allowed since they are erased at compile time. The rule is scoped to `src/**/*.ts` so scripts and tests are unaffected. This prevents the class of bugs described in #1123 where direct imports work in development (devDependencies present) but fail in the published npm package (only bundled code available). Closes #1123
1 parent e6b7a09 commit 4dacf02

3 files changed

Lines changed: 109 additions & 1 deletion

File tree

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+
name: 'Bundle import guard',
139+
files: ['src/**/*.ts'],
140+
rules: {
141+
'@local/no-direct-third-party-imports': 'error',
142+
},
143+
},
144+
{
138145
name: 'Tests',
139146
files: ['**/*.test.ts'],
140147
rules: {

scripts/eslint_rules/local-plugin.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,11 @@
55
*/
66

77
import checkLicenseRule from './check-license-rule.js';
8+
import noDirectThirdPartyImportsRule from './no-direct-third-party-imports-rule.js';
89

9-
export default {rules: {'check-license': checkLicenseRule}};
10+
export default {
11+
rules: {
12+
'check-license': checkLicenseRule,
13+
'no-direct-third-party-imports': noDirectThirdPartyImportsRule,
14+
},
15+
};
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
* See https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/1123
19+
*/
20+
21+
const THIRD_PARTY_PACKAGES = [
22+
'@modelcontextprotocol/sdk',
23+
'puppeteer-core',
24+
'@puppeteer/browsers',
25+
'yargs',
26+
'debug',
27+
'zod',
28+
'core-js',
29+
];
30+
31+
/** Matches any import source that starts with one of the restricted packages. */
32+
function isRestrictedSource(source) {
33+
return THIRD_PARTY_PACKAGES.some(
34+
pkg => source === pkg || source.startsWith(pkg + '/'),
35+
);
36+
}
37+
38+
/** Returns true when the file is inside src/third_party/. */
39+
function isThirdPartyBarrel(filename) {
40+
const normalized = filename.replace(/\\/g, '/');
41+
return normalized.includes('/src/third_party/');
42+
}
43+
44+
export default {
45+
name: 'no-direct-third-party-imports',
46+
meta: {
47+
type: 'problem',
48+
docs: {
49+
description:
50+
'Disallow value imports of bundled third-party packages outside of src/third_party/',
51+
},
52+
schema: [],
53+
messages: {
54+
noDirectImport:
55+
'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.)',
56+
},
57+
},
58+
defaultOptions: [],
59+
create(context) {
60+
const filename = context.getFilename();
61+
if (isThirdPartyBarrel(filename)) {
62+
return {};
63+
}
64+
65+
return {
66+
ImportDeclaration(node) {
67+
// `import type { Foo } from '...'` is always safe.
68+
if (node.importKind === 'type') {
69+
return;
70+
}
71+
72+
const source = node.source.value;
73+
if (!isRestrictedSource(source)) {
74+
return;
75+
}
76+
77+
// If every specifier is `type`, the import is still safe.
78+
// e.g. `import { type Foo, type Bar } from '...'`
79+
const hasValueSpecifier = node.specifiers.some(
80+
s => s.type !== 'ImportSpecifier' || s.importKind !== 'type',
81+
);
82+
83+
if (!hasValueSpecifier) {
84+
return;
85+
}
86+
87+
context.report({
88+
node,
89+
messageId: 'noDirectImport',
90+
data: {source},
91+
});
92+
},
93+
};
94+
},
95+
};

0 commit comments

Comments
 (0)