feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import path, { relative } from 'path';
|
||||
import { Rule } from 'eslint';
|
||||
import readPkgUp from 'eslint-module-utils/readPkgUp';
|
||||
import resolve from 'eslint-module-utils/resolve';
|
||||
import { exportPathMatch } from './utils';
|
||||
|
||||
export const noPkgDirImport: Rule.RuleModule = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'limit import package directory directly',
|
||||
},
|
||||
messages: {
|
||||
invalidSubpath:
|
||||
'subPath `{{ subPath }}` is NOT exported in `{{ pkg }}`, you can config the `exports` fields in package.json',
|
||||
noExportsCfg:
|
||||
"NO `exports` fields config in `{{ pkg }}` package.json, you can't import by subPath ",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = `${node.source.value}`;
|
||||
const modulePath = resolve(importPath, context);
|
||||
|
||||
if (!modulePath) {
|
||||
// 解析不到的情况,暂不处理
|
||||
return;
|
||||
}
|
||||
|
||||
const { pkg, path: importPkgPath } = readPkgUp({
|
||||
cwd: modulePath,
|
||||
}) as any;
|
||||
|
||||
const { path: currentPkgPath } = readPkgUp({
|
||||
cwd: context.filename,
|
||||
}) as any;
|
||||
|
||||
if (!pkg.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 本地link会解析到node_modules目录,需要拿到pkg name再次解析。
|
||||
const moduleRealPath = resolve(pkg.name, context);
|
||||
|
||||
if (
|
||||
// 包名称就是引用路径
|
||||
pkg.name === importPath ||
|
||||
// 解析到其他包,如@type
|
||||
!importPath.startsWith(pkg.name) ||
|
||||
// 解析到自己包的文件
|
||||
currentPkgPath === importPkgPath ||
|
||||
!moduleRealPath ||
|
||||
moduleRealPath.includes('node_modules')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pkg.exports) {
|
||||
context.report({
|
||||
messageId: 'noExportsCfg',
|
||||
data: {
|
||||
pkg: pkg.name,
|
||||
},
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
loc: node.loc,
|
||||
});
|
||||
} else if (pkg.exports) {
|
||||
if (typeof pkg.exports === 'string') {
|
||||
context.report({
|
||||
messageId: 'noExportsCfg',
|
||||
data: {
|
||||
pkg: pkg.name,
|
||||
},
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
loc: node.loc,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const validSubPath = Object.keys(pkg.exports);
|
||||
if (
|
||||
!validSubPath.some(p => {
|
||||
const pkgExportPath = path.join(pkg.name, p);
|
||||
return exportPathMatch(importPath, pkgExportPath);
|
||||
})
|
||||
) {
|
||||
const subPath = relative(pkg.name, importPath);
|
||||
context.report({
|
||||
messageId: 'invalidSubpath',
|
||||
data: {
|
||||
subPath,
|
||||
pkg: pkg.name,
|
||||
},
|
||||
// @ts-expect-error -- linter-disable-autofix
|
||||
loc: node.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { RuleTester } from 'eslint';
|
||||
import resolve from 'eslint-module-utils/resolve';
|
||||
import readPkgUp from 'eslint-module-utils/readPkgUp';
|
||||
import { noPkgDirImport } from '../index';
|
||||
|
||||
const ruleTester = new RuleTester({});
|
||||
|
||||
vi.mock('eslint-module-utils/resolve', () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('eslint-module-utils/readPkgUp', () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
const validCases = [
|
||||
{
|
||||
code: 'import "xxx"',
|
||||
modulePath: undefined, // modulePath 为 空
|
||||
moduleRealPath: undefined,
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: '',
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'some/pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: 'path/to/module',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'some/pkg', // 包名称与引用路径相同
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'some/pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: 'path/to/module',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: undefined, // 解析到不规范配置的package.json
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: 'path/to/module',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: '@types/pkg', // 解析到类型包
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: 'path/to/module',
|
||||
importPkgPath: 'path/to/same/pkg', // 相同路径
|
||||
currentPkgPath: 'path/to/same/pkg',
|
||||
pkg: {
|
||||
name: '@types/pkg',
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: undefined,
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg';",
|
||||
modulePath: 'path/to/module',
|
||||
moduleRealPath: 'path/to/node_modules/pkg', // 解析到node_modules
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg/subPath';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: { subPath: './subPath' },
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg/sub/path';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: { 'sub/*': './subPath' },
|
||||
},
|
||||
},
|
||||
].map(c => {
|
||||
vi.mocked(resolve).mockReturnValueOnce(c.modulePath);
|
||||
|
||||
if (!c.modulePath) {
|
||||
return {
|
||||
code: c.code,
|
||||
// TODO: 避免eslint duplication检测。可能需要改为其他方式
|
||||
settings: c,
|
||||
};
|
||||
}
|
||||
|
||||
if (c.pkg.name) {
|
||||
vi.mocked(resolve).mockReturnValueOnce(c.moduleRealPath);
|
||||
}
|
||||
|
||||
vi.mocked(readPkgUp)
|
||||
.mockReturnValueOnce({
|
||||
pkg: c.pkg,
|
||||
path: c.importPkgPath,
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
path: c.currentPkgPath,
|
||||
});
|
||||
|
||||
return {
|
||||
code: c.code,
|
||||
settings: c,
|
||||
};
|
||||
});
|
||||
|
||||
const invalidCases = [
|
||||
{
|
||||
code: "import pkg from 'pkg/subPath';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: undefined, // 为空
|
||||
},
|
||||
messageId: 'noExportsCfg',
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg/subPath';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: 'main.js', // isString
|
||||
},
|
||||
messageId: 'noExportsCfg',
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg/subPath';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: { otherPath: 'otherPath' },
|
||||
},
|
||||
messageId: 'invalidSubpath',
|
||||
},
|
||||
{
|
||||
code: "import pkg from 'pkg/sub/path';",
|
||||
modulePath: 'path/to/pkg',
|
||||
moduleRealPath: 'path/to/pkg',
|
||||
importPkgPath: 'path/to/import/pkg',
|
||||
currentPkgPath: 'path/to/current/pkg',
|
||||
pkg: {
|
||||
name: 'pkg',
|
||||
exports: {
|
||||
sub: './sub',
|
||||
},
|
||||
},
|
||||
messageId: 'invalidSubpath',
|
||||
},
|
||||
].map(c => {
|
||||
vi.mocked(resolve).mockReturnValueOnce(c.modulePath);
|
||||
|
||||
if (!c.modulePath) {
|
||||
return {
|
||||
settings: c,
|
||||
code: c.code,
|
||||
errors: [],
|
||||
};
|
||||
}
|
||||
|
||||
vi.mocked(resolve).mockReturnValueOnce(c.moduleRealPath);
|
||||
|
||||
vi.mocked(readPkgUp)
|
||||
.mockReturnValueOnce({
|
||||
pkg: c.pkg,
|
||||
path: c.importPkgPath,
|
||||
})
|
||||
.mockReturnValueOnce({
|
||||
path: c.currentPkgPath,
|
||||
});
|
||||
return {
|
||||
settings: c,
|
||||
code: c.code,
|
||||
errors: [
|
||||
{
|
||||
messageId: c.messageId,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
ruleTester.run('no-pkg-dir-import', noPkgDirImport, {
|
||||
valid: [...validCases],
|
||||
invalid: [...invalidCases],
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { exportPathMatch } from '../utils';
|
||||
|
||||
describe('exportPathMatch', () => {
|
||||
it.each([
|
||||
['./foo', './foo'],
|
||||
['./foo.js', './*'],
|
||||
['./foo.js', './*.js'],
|
||||
['./foo/baz', './foo/*'],
|
||||
['./foo/baz/baz.js', './foo/*'],
|
||||
])(
|
||||
'import path is %s, export path is %s, should be matched',
|
||||
(importPath, exportPath) => {
|
||||
expect(exportPathMatch(importPath, exportPath)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
['./foo', './bar'],
|
||||
['./foo.js', './*.ts'],
|
||||
['./foo.js', './foo.ts'],
|
||||
['./baz/bar', './foo/*'],
|
||||
['./foo/bar/baz.js', './foo/*.js'],
|
||||
])(
|
||||
'import path is %s, export path is %s, should NOT be matched',
|
||||
(importPath, exportPath) => {
|
||||
expect(exportPathMatch(importPath, exportPath)).toBe(false);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
|
||||
export function exportPathMatch(importPath: string, pkgExportPath: string) {
|
||||
if (importPath === pkgExportPath) {
|
||||
return true;
|
||||
}
|
||||
const pkgExportBasename = path.basename(pkgExportPath);
|
||||
|
||||
if (importPath.startsWith(path.dirname(pkgExportPath))) {
|
||||
if (pkgExportBasename === '*') {
|
||||
return true;
|
||||
}
|
||||
if (path.dirname(importPath) === path.dirname(pkgExportPath)) {
|
||||
return pkgExportBasename === `*${path.extname(importPath)}`;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Reference in New Issue
Block a user