@ankhorage/devtools 1.5.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tools/eslint/index.js +6 -4
- package/dist/tools/eslint/moduleOwnership.d.ts +1 -1
- package/dist/tools/eslint/moduleOwnership.js +37 -19
- package/dist/tools/eslint/packageEntrypoints.d.ts +2 -0
- package/dist/tools/eslint/packageEntrypoints.js +47 -0
- package/dist/tools/eslint/packageJsonPath.d.ts +2 -0
- package/dist/tools/eslint/packageJsonPath.js +17 -0
- package/dist/tools/eslint/profile.js +8 -31
- package/package.json +1 -1
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* Every profile includes the shared TypeScript, import, unused-import, Prettier, security, and
|
|
9
9
|
* quality rules. The common quality limits are 50 effective lines per function, 300 effective
|
|
10
10
|
* lines per file, and modified cyclomatic complexity 15. Forward exports are forbidden outside
|
|
11
|
-
* explicit `index.*` barrels so implementation files export only
|
|
12
|
-
* React and Hooks correctness rules; React Native composes the React
|
|
13
|
-
* React Native rules.
|
|
11
|
+
* declared package entrypoints and explicit `index.*` barrels so implementation files export only
|
|
12
|
+
* symbols they own. React adds React and Hooks correctness rules; React Native composes the React
|
|
13
|
+
* profile and adds focused React Native rules.
|
|
14
14
|
*
|
|
15
15
|
* Repository-specific behavior stays additive: `additionalIgnores`, `restrictedImports`, and
|
|
16
16
|
* `overrides` extend the central policy instead of replacing it. Narrow local overrides remain the
|
|
@@ -33,6 +33,7 @@ import simpleImportSort from 'eslint-plugin-simple-import-sort';
|
|
|
33
33
|
import unusedImports from 'eslint-plugin-unused-imports';
|
|
34
34
|
import tseslint from 'typescript-eslint';
|
|
35
35
|
import { createModuleOwnershipConfig } from './moduleOwnership.js';
|
|
36
|
+
import { resolvePackageEntrypointFiles } from './packageEntrypoints.js';
|
|
36
37
|
import { resolveEslintProfile } from './profile.js';
|
|
37
38
|
export const defaultIgnores = [
|
|
38
39
|
'**/ios/**',
|
|
@@ -55,7 +56,8 @@ export const defaultRestrictedImports = [
|
|
|
55
56
|
export function createConfig(options) {
|
|
56
57
|
const normalized = normalizeOptions(options);
|
|
57
58
|
const profile = resolveEslintProfile(options);
|
|
58
|
-
|
|
59
|
+
const packageEntrypoints = resolvePackageEntrypointFiles(options);
|
|
60
|
+
return defineConfig({ ignores: [...defaultIgnores, ...normalized.additionalIgnores] }, { ...js.configs.recommended, files: normalized.files }, ...createTypeCheckedConfigs(normalized), createBaseConfig(normalized), createModuleOwnershipConfig(normalized.files, packageEntrypoints), ...createProfileConfigs(profile, normalized.files), ...normalized.overrides, ...(normalized.includePrettier ? [prettierConfig] : []));
|
|
59
61
|
}
|
|
60
62
|
function normalizeOptions(options) {
|
|
61
63
|
return {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { FlatConfigItem } from './types.js';
|
|
2
|
-
export declare function createModuleOwnershipConfig(files: string[]): FlatConfigItem;
|
|
2
|
+
export declare function createModuleOwnershipConfig(files: string[], packageEntrypoints?: string[]): FlatConfigItem;
|
|
@@ -1,34 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
'**/index.tsx',
|
|
4
|
-
'**/index.js',
|
|
5
|
-
'**/index.jsx',
|
|
6
|
-
'**/index.mts',
|
|
7
|
-
'**/index.cts',
|
|
8
|
-
'**/index.mjs',
|
|
9
|
-
'**/index.cjs',
|
|
10
|
-
];
|
|
1
|
+
import { basename, normalize } from 'node:path';
|
|
2
|
+
const INDEX_BARREL_FILE = /^index\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)$/u;
|
|
11
3
|
function isForwardExport(context, node) {
|
|
12
4
|
const tokens = context.sourceCode.getTokens(node);
|
|
13
|
-
if (tokens.at(0)?.value !== 'export')
|
|
5
|
+
if (tokens.at(0)?.value !== 'export')
|
|
14
6
|
return false;
|
|
15
|
-
}
|
|
16
7
|
return tokens.some((token, index) => token.value === 'from' && tokens.at(index + 1)?.type === 'String');
|
|
17
8
|
}
|
|
9
|
+
function isAllowedForwardExportFile(context) {
|
|
10
|
+
const filename = normalize(context.filename);
|
|
11
|
+
if (INDEX_BARREL_FILE.test(basename(filename)))
|
|
12
|
+
return true;
|
|
13
|
+
return readAllowedFiles(context.options.at(0)).some((file) => normalize(file) === filename);
|
|
14
|
+
}
|
|
15
|
+
function readAllowedFiles(value) {
|
|
16
|
+
if (!isRecord(value))
|
|
17
|
+
return [];
|
|
18
|
+
const { allowedFiles } = value;
|
|
19
|
+
if (!Array.isArray(allowedFiles))
|
|
20
|
+
return [];
|
|
21
|
+
return allowedFiles.filter((file) => typeof file === 'string');
|
|
22
|
+
}
|
|
23
|
+
function isRecord(value) {
|
|
24
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
|
+
}
|
|
18
26
|
const noForwardExportsRule = {
|
|
19
27
|
meta: {
|
|
20
28
|
type: 'problem',
|
|
21
|
-
schema: [
|
|
29
|
+
schema: [
|
|
30
|
+
{
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
allowedFiles: { type: 'array', items: { type: 'string' } },
|
|
34
|
+
},
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
22
38
|
messages: {
|
|
23
|
-
forwardExport: 'Forward exports are forbidden outside index barrels. Import directly from the owning module.',
|
|
39
|
+
forwardExport: 'Forward exports are forbidden outside package entrypoints and index barrels. Import directly from the owning module.',
|
|
24
40
|
},
|
|
25
41
|
},
|
|
26
42
|
create(context) {
|
|
43
|
+
if (isAllowedForwardExportFile(context))
|
|
44
|
+
return {};
|
|
27
45
|
return {
|
|
28
46
|
'Program > *'(node) {
|
|
29
|
-
if (isForwardExport(context, node))
|
|
47
|
+
if (isForwardExport(context, node))
|
|
30
48
|
context.report({ node, messageId: 'forwardExport' });
|
|
31
|
-
}
|
|
32
49
|
},
|
|
33
50
|
};
|
|
34
51
|
},
|
|
@@ -36,11 +53,12 @@ const noForwardExportsRule = {
|
|
|
36
53
|
const moduleOwnershipPlugin = {
|
|
37
54
|
rules: { 'no-forward-exports': noForwardExportsRule },
|
|
38
55
|
};
|
|
39
|
-
export function createModuleOwnershipConfig(files) {
|
|
56
|
+
export function createModuleOwnershipConfig(files, packageEntrypoints = []) {
|
|
40
57
|
return {
|
|
41
58
|
files,
|
|
42
|
-
ignores: [...INDEX_BARREL_FILES],
|
|
43
59
|
plugins: { ankhorage: moduleOwnershipPlugin },
|
|
44
|
-
rules: {
|
|
60
|
+
rules: {
|
|
61
|
+
'ankhorage/no-forward-exports': ['error', { allowedFiles: packageEntrypoints }],
|
|
62
|
+
},
|
|
45
63
|
};
|
|
46
64
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
import { resolveProjectPackageJsonPath } from './packageJsonPath.js';
|
|
4
|
+
const SOURCE_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'mts', 'cts', 'mjs', 'cjs'];
|
|
5
|
+
const OUTPUT_EXTENSION = /(?:\.d)?\.(?:ts|tsx|js|jsx|mts|cts|mjs|cjs)$/u;
|
|
6
|
+
export function resolvePackageEntrypointFiles(options) {
|
|
7
|
+
const packageJsonPath = resolveProjectPackageJsonPath(options);
|
|
8
|
+
if (packageJsonPath === null)
|
|
9
|
+
return [];
|
|
10
|
+
const packageJson = readPackageJson(packageJsonPath);
|
|
11
|
+
const targets = [
|
|
12
|
+
...collectStringTargets(packageJson.main),
|
|
13
|
+
...collectStringTargets(packageJson.types),
|
|
14
|
+
...collectStringTargets(packageJson.exports),
|
|
15
|
+
];
|
|
16
|
+
return [...new Set(targets.flatMap((target) => toSourceCandidates(target, packageJsonPath)))];
|
|
17
|
+
}
|
|
18
|
+
function readPackageJson(packageJsonPath) {
|
|
19
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
20
|
+
if (!isRecord(parsed)) {
|
|
21
|
+
throw new Error(`Expected package.json to contain a JSON object: ${packageJsonPath}`);
|
|
22
|
+
}
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
function collectStringTargets(value) {
|
|
26
|
+
if (typeof value === 'string')
|
|
27
|
+
return [value];
|
|
28
|
+
if (Array.isArray(value))
|
|
29
|
+
return value.flatMap(collectStringTargets);
|
|
30
|
+
if (!isRecord(value))
|
|
31
|
+
return [];
|
|
32
|
+
return Object.values(value).flatMap(collectStringTargets);
|
|
33
|
+
}
|
|
34
|
+
function toSourceCandidates(target, packageJsonPath) {
|
|
35
|
+
const normalizedTarget = target.replace(/^\.\//u, '');
|
|
36
|
+
const sourceTarget = normalizedTarget.startsWith('dist/')
|
|
37
|
+
? `src/${normalizedTarget.slice('dist/'.length)}`
|
|
38
|
+
: normalizedTarget;
|
|
39
|
+
if (!sourceTarget.startsWith('src/'))
|
|
40
|
+
return [];
|
|
41
|
+
const sourceBase = sourceTarget.replace(OUTPUT_EXTENSION, '');
|
|
42
|
+
const absoluteBase = resolve(dirname(packageJsonPath), sourceBase);
|
|
43
|
+
return SOURCE_EXTENSIONS.map((extension) => `${absoluteBase}.${extension}`);
|
|
44
|
+
}
|
|
45
|
+
function isRecord(value) {
|
|
46
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, resolve } from 'node:path';
|
|
3
|
+
export function resolveProjectPackageJsonPath(options) {
|
|
4
|
+
if (options.packageJsonPath !== undefined) {
|
|
5
|
+
return resolve(options.tsconfigRootDir, options.packageJsonPath);
|
|
6
|
+
}
|
|
7
|
+
let directory = resolve(options.tsconfigRootDir);
|
|
8
|
+
for (;;) {
|
|
9
|
+
const candidate = resolve(directory, 'package.json');
|
|
10
|
+
if (existsSync(candidate))
|
|
11
|
+
return candidate;
|
|
12
|
+
const parent = dirname(directory);
|
|
13
|
+
if (parent === directory)
|
|
14
|
+
return null;
|
|
15
|
+
directory = parent;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -1,42 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, resolve } from 'node:path';
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
3
2
|
import { detectProject, } from '@ankhorage/utility/project';
|
|
3
|
+
import { resolveProjectPackageJsonPath } from './packageJsonPath.js';
|
|
4
4
|
export function resolveEslintProfile(options) {
|
|
5
5
|
const requestedProfile = options.profile ?? 'auto';
|
|
6
|
-
if (requestedProfile !== 'auto')
|
|
6
|
+
if (requestedProfile !== 'auto')
|
|
7
7
|
return requestedProfile;
|
|
8
|
-
}
|
|
9
8
|
const packageJsonPath = resolveProjectPackageJsonPath(options);
|
|
10
9
|
const input = packageJsonPath === null ? {} : readDetectionInput(packageJsonPath);
|
|
11
10
|
return resolveEslintProfileFromDetectionInput('auto', input);
|
|
12
11
|
}
|
|
13
12
|
export function resolveEslintProfileFromDetectionInput(requestedProfile, input) {
|
|
14
|
-
if (requestedProfile !== 'auto')
|
|
13
|
+
if (requestedProfile !== 'auto')
|
|
15
14
|
return requestedProfile;
|
|
16
|
-
}
|
|
17
15
|
const { traits } = detectProject(input);
|
|
18
|
-
if (traits.has('react-native') || traits.has('expo'))
|
|
16
|
+
if (traits.has('react-native') || traits.has('expo'))
|
|
19
17
|
return 'react-native';
|
|
20
|
-
}
|
|
21
18
|
return traits.has('react') ? 'react' : 'base';
|
|
22
19
|
}
|
|
23
|
-
function resolveProjectPackageJsonPath(options) {
|
|
24
|
-
if (options.packageJsonPath !== undefined) {
|
|
25
|
-
return resolve(options.tsconfigRootDir, options.packageJsonPath);
|
|
26
|
-
}
|
|
27
|
-
let directory = resolve(options.tsconfigRootDir);
|
|
28
|
-
for (;;) {
|
|
29
|
-
const candidate = resolve(directory, 'package.json');
|
|
30
|
-
if (existsSync(candidate)) {
|
|
31
|
-
return candidate;
|
|
32
|
-
}
|
|
33
|
-
const parent = dirname(directory);
|
|
34
|
-
if (parent === directory) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
directory = parent;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
20
|
function readDetectionInput(packageJsonPath) {
|
|
41
21
|
const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
42
22
|
if (!isRecord(parsed)) {
|
|
@@ -55,21 +35,18 @@ function optionalDependencyMap(key, value) {
|
|
|
55
35
|
return dependencyMap === undefined ? {} : { [key]: dependencyMap };
|
|
56
36
|
}
|
|
57
37
|
function toDependencyMap(value) {
|
|
58
|
-
if (!isRecord(value))
|
|
38
|
+
if (!isRecord(value))
|
|
59
39
|
return undefined;
|
|
60
|
-
}
|
|
61
40
|
const dependencies = {};
|
|
62
41
|
for (const [name, version] of Object.entries(value)) {
|
|
63
|
-
if (typeof version === 'string')
|
|
42
|
+
if (typeof version === 'string')
|
|
64
43
|
dependencies[name] = version;
|
|
65
|
-
}
|
|
66
44
|
}
|
|
67
45
|
return dependencies;
|
|
68
46
|
}
|
|
69
47
|
function optionalEngines(value) {
|
|
70
|
-
if (!isRecord(value))
|
|
48
|
+
if (!isRecord(value))
|
|
71
49
|
return {};
|
|
72
|
-
}
|
|
73
50
|
const engines = {
|
|
74
51
|
...(typeof value.bun === 'string' ? { bun: value.bun } : {}),
|
|
75
52
|
...(typeof value.node === 'string' ? { node: value.node } : {}),
|
package/package.json
CHANGED