@idealyst/tooling 1.2.3
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/README.md +185 -0
- package/package.json +89 -0
- package/src/analyzer/component-analyzer.ts +418 -0
- package/src/analyzer/index.ts +16 -0
- package/src/analyzer/theme-analyzer.ts +473 -0
- package/src/analyzer/types.ts +132 -0
- package/src/analyzers/index.ts +1 -0
- package/src/analyzers/platformImports.ts +395 -0
- package/src/index.ts +142 -0
- package/src/rules/index.ts +2 -0
- package/src/rules/reactDomPrimitives.ts +217 -0
- package/src/rules/reactNativePrimitives.ts +363 -0
- package/src/types.ts +173 -0
- package/src/utils/fileClassifier.ts +135 -0
- package/src/utils/importParser.ts +235 -0
- package/src/utils/index.ts +2 -0
- package/src/vite-plugin.ts +264 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File type classification based on file extension patterns
|
|
3
|
+
*/
|
|
4
|
+
export type FileType =
|
|
5
|
+
| 'shared' // .tsx, .jsx - shared cross-platform files
|
|
6
|
+
| 'web' // .web.tsx, .web.jsx - web-specific files
|
|
7
|
+
| 'native' // .native.tsx, .native.jsx - React Native-specific files
|
|
8
|
+
| 'styles' // .styles.tsx, .styles.ts - style definition files
|
|
9
|
+
| 'types' // .types.ts, types.ts - type definition files
|
|
10
|
+
| 'other'; // non-component files
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Platform classification for imports
|
|
14
|
+
*/
|
|
15
|
+
export type Platform = 'react-native' | 'react-dom' | 'neutral';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Violation type describing what kind of platform mismatch occurred
|
|
19
|
+
*/
|
|
20
|
+
export type ViolationType =
|
|
21
|
+
| 'native-in-shared' // React Native primitive used in shared file
|
|
22
|
+
| 'dom-in-shared' // React DOM primitive used in shared file
|
|
23
|
+
| 'native-in-web' // React Native primitive used in web file
|
|
24
|
+
| 'dom-in-native'; // React DOM primitive used in native file
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Severity level for violations
|
|
28
|
+
*/
|
|
29
|
+
export type Severity = 'error' | 'warning' | 'info';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Information about a single import
|
|
33
|
+
*/
|
|
34
|
+
export interface ImportInfo {
|
|
35
|
+
/** The imported identifier name */
|
|
36
|
+
name: string;
|
|
37
|
+
/** Original name if aliased (e.g., `Image as RNImage`) */
|
|
38
|
+
originalName?: string;
|
|
39
|
+
/** The module source (e.g., 'react-native', 'react-dom') */
|
|
40
|
+
source: string;
|
|
41
|
+
/** The platform this import belongs to */
|
|
42
|
+
platform: Platform;
|
|
43
|
+
/** Line number in source file */
|
|
44
|
+
line: number;
|
|
45
|
+
/** Column number in source file */
|
|
46
|
+
column: number;
|
|
47
|
+
/** Whether this is a default import */
|
|
48
|
+
isDefault: boolean;
|
|
49
|
+
/** Whether this is a namespace import (import * as X) */
|
|
50
|
+
isNamespace: boolean;
|
|
51
|
+
/** Whether this is a type-only import */
|
|
52
|
+
isTypeOnly: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A single violation found during analysis
|
|
57
|
+
*/
|
|
58
|
+
export interface Violation {
|
|
59
|
+
/** Type of violation */
|
|
60
|
+
type: ViolationType;
|
|
61
|
+
/** The primitive/component that caused the violation */
|
|
62
|
+
primitive: string;
|
|
63
|
+
/** The module source (e.g., 'react-native', 'react-dom') */
|
|
64
|
+
source: string;
|
|
65
|
+
/** Path to the file with the violation */
|
|
66
|
+
filePath: string;
|
|
67
|
+
/** Line number where violation occurred */
|
|
68
|
+
line: number;
|
|
69
|
+
/** Column number where violation occurred */
|
|
70
|
+
column: number;
|
|
71
|
+
/** Human-readable message describing the violation */
|
|
72
|
+
message: string;
|
|
73
|
+
/** Severity level of this violation */
|
|
74
|
+
severity: Severity;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Result of analyzing a single file
|
|
79
|
+
*/
|
|
80
|
+
export interface AnalysisResult {
|
|
81
|
+
/** Path to the analyzed file */
|
|
82
|
+
filePath: string;
|
|
83
|
+
/** Classified type of the file */
|
|
84
|
+
fileType: FileType;
|
|
85
|
+
/** List of violations found */
|
|
86
|
+
violations: Violation[];
|
|
87
|
+
/** All imports found in the file */
|
|
88
|
+
imports: ImportInfo[];
|
|
89
|
+
/** Whether the file passed validation (no violations) */
|
|
90
|
+
passed: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Options for configuring the platform import analyzer
|
|
95
|
+
*/
|
|
96
|
+
export interface AnalyzerOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Default severity level for violations
|
|
99
|
+
* @default 'error'
|
|
100
|
+
*/
|
|
101
|
+
severity?: Severity;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Additional React Native primitives to flag beyond the built-in list
|
|
105
|
+
* Useful for flagging custom native-only components
|
|
106
|
+
*/
|
|
107
|
+
additionalNativePrimitives?: string[];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Additional React DOM primitives to flag beyond the built-in list
|
|
111
|
+
* Useful for flagging custom web-only components
|
|
112
|
+
*/
|
|
113
|
+
additionalDomPrimitives?: string[];
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Primitives to ignore/allow even if they would normally be flagged
|
|
117
|
+
*/
|
|
118
|
+
ignoredPrimitives?: string[];
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Glob patterns for files to skip analysis on
|
|
122
|
+
* @example ['**\/*.test.tsx', '**\/*.stories.tsx']
|
|
123
|
+
*/
|
|
124
|
+
ignoredPatterns?: string[];
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Additional module sources to treat as React Native
|
|
128
|
+
* @example ['react-native-gesture-handler', 'react-native-reanimated']
|
|
129
|
+
*/
|
|
130
|
+
additionalNativeSources?: string[];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Additional module sources to treat as React DOM
|
|
134
|
+
* @example ['react-dom/client']
|
|
135
|
+
*/
|
|
136
|
+
additionalDomSources?: string[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Input for batch file analysis
|
|
141
|
+
*/
|
|
142
|
+
export interface FileInput {
|
|
143
|
+
/** File path */
|
|
144
|
+
path: string;
|
|
145
|
+
/** File content (source code) */
|
|
146
|
+
content: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Primitive rule definition
|
|
151
|
+
*/
|
|
152
|
+
export interface PrimitiveRule {
|
|
153
|
+
/** Name of the primitive/component */
|
|
154
|
+
name: string;
|
|
155
|
+
/** Module source it comes from */
|
|
156
|
+
source: string;
|
|
157
|
+
/** Platform it belongs to */
|
|
158
|
+
platform: Platform;
|
|
159
|
+
/** Optional description of why it's platform-specific */
|
|
160
|
+
description?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Rule set containing primitives for a specific platform
|
|
165
|
+
*/
|
|
166
|
+
export interface PrimitiveRuleSet {
|
|
167
|
+
/** Platform these rules apply to */
|
|
168
|
+
platform: Platform;
|
|
169
|
+
/** List of primitive rules */
|
|
170
|
+
primitives: PrimitiveRule[];
|
|
171
|
+
/** Module sources that indicate this platform */
|
|
172
|
+
sources: string[];
|
|
173
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { FileType } from '../types';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Extension patterns for classification
|
|
6
|
+
* Order matters - more specific patterns should come first
|
|
7
|
+
*/
|
|
8
|
+
const EXTENSION_PATTERNS: Array<{ pattern: RegExp; type: FileType }> = [
|
|
9
|
+
// Platform-specific component files
|
|
10
|
+
{ pattern: /\.web\.(tsx|jsx)$/, type: 'web' },
|
|
11
|
+
{ pattern: /\.native\.(tsx|jsx)$/, type: 'native' },
|
|
12
|
+
{ pattern: /\.ios\.(tsx|jsx)$/, type: 'native' },
|
|
13
|
+
{ pattern: /\.android\.(tsx|jsx)$/, type: 'native' },
|
|
14
|
+
|
|
15
|
+
// Style files (can be .ts or .tsx)
|
|
16
|
+
{ pattern: /\.styles?\.(tsx?|jsx?)$/, type: 'styles' },
|
|
17
|
+
|
|
18
|
+
// Type definition files
|
|
19
|
+
{ pattern: /\.types?\.(ts|tsx)$/, type: 'types' },
|
|
20
|
+
{ pattern: /types\.(ts|tsx)$/, type: 'types' },
|
|
21
|
+
{ pattern: /\.d\.ts$/, type: 'types' },
|
|
22
|
+
|
|
23
|
+
// Shared component files (generic .tsx/.jsx without platform suffix)
|
|
24
|
+
{ pattern: /\.(tsx|jsx)$/, type: 'shared' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Files that should be classified as 'other' regardless of extension
|
|
29
|
+
*/
|
|
30
|
+
const EXCLUDED_PATTERNS: RegExp[] = [
|
|
31
|
+
/\.test\.(tsx?|jsx?)$/,
|
|
32
|
+
/\.spec\.(tsx?|jsx?)$/,
|
|
33
|
+
/\.stories\.(tsx?|jsx?)$/,
|
|
34
|
+
/\.config\.(ts|js)$/,
|
|
35
|
+
/index\.(ts|tsx|js|jsx)$/,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Classifies a file based on its path and extension
|
|
40
|
+
*
|
|
41
|
+
* @param filePath - The file path to classify
|
|
42
|
+
* @returns The file type classification
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* classifyFile('Button.tsx') // 'shared'
|
|
46
|
+
* classifyFile('Button.web.tsx') // 'web'
|
|
47
|
+
* classifyFile('Button.native.tsx') // 'native'
|
|
48
|
+
* classifyFile('Button.styles.tsx') // 'styles'
|
|
49
|
+
* classifyFile('types.ts') // 'types'
|
|
50
|
+
*/
|
|
51
|
+
export function classifyFile(filePath: string): FileType {
|
|
52
|
+
const fileName = path.basename(filePath);
|
|
53
|
+
|
|
54
|
+
// Check if this file should be excluded from component analysis
|
|
55
|
+
for (const pattern of EXCLUDED_PATTERNS) {
|
|
56
|
+
if (pattern.test(fileName)) {
|
|
57
|
+
return 'other';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Match against extension patterns
|
|
62
|
+
for (const { pattern, type } of EXTENSION_PATTERNS) {
|
|
63
|
+
if (pattern.test(fileName)) {
|
|
64
|
+
return type;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return 'other';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checks if a file is a component file that should be analyzed
|
|
73
|
+
*
|
|
74
|
+
* @param filePath - The file path to check
|
|
75
|
+
* @returns True if the file is a component file (.tsx or .jsx)
|
|
76
|
+
*/
|
|
77
|
+
export function isComponentFile(filePath: string): boolean {
|
|
78
|
+
const fileType = classifyFile(filePath);
|
|
79
|
+
return fileType === 'shared' || fileType === 'web' || fileType === 'native';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Checks if a file is a shared (cross-platform) component file
|
|
84
|
+
* These are the files that should NOT contain platform-specific imports
|
|
85
|
+
*
|
|
86
|
+
* @param filePath - The file path to check
|
|
87
|
+
* @returns True if the file is a shared component file
|
|
88
|
+
*/
|
|
89
|
+
export function isSharedFile(filePath: string): boolean {
|
|
90
|
+
return classifyFile(filePath) === 'shared';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Checks if a file is platform-specific
|
|
95
|
+
*
|
|
96
|
+
* @param filePath - The file path to check
|
|
97
|
+
* @returns True if the file is web or native specific
|
|
98
|
+
*/
|
|
99
|
+
export function isPlatformSpecificFile(filePath: string): boolean {
|
|
100
|
+
const fileType = classifyFile(filePath);
|
|
101
|
+
return fileType === 'web' || fileType === 'native';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Gets the expected platform for a file
|
|
106
|
+
*
|
|
107
|
+
* @param filePath - The file path to check
|
|
108
|
+
* @returns The expected platform, or null for shared/other files
|
|
109
|
+
*/
|
|
110
|
+
export function getExpectedPlatform(filePath: string): 'web' | 'native' | null {
|
|
111
|
+
const fileType = classifyFile(filePath);
|
|
112
|
+
if (fileType === 'web') return 'web';
|
|
113
|
+
if (fileType === 'native') return 'native';
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Extracts the base component name from a file path
|
|
119
|
+
*
|
|
120
|
+
* @param filePath - The file path
|
|
121
|
+
* @returns The base component name without platform suffix or extension
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* getBaseName('Button.web.tsx') // 'Button'
|
|
125
|
+
* getBaseName('Button.native.tsx') // 'Button'
|
|
126
|
+
* getBaseName('Button.tsx') // 'Button'
|
|
127
|
+
*/
|
|
128
|
+
export function getBaseName(filePath: string): string {
|
|
129
|
+
const fileName = path.basename(filePath);
|
|
130
|
+
return fileName
|
|
131
|
+
.replace(/\.(web|native|ios|android)\.(tsx|jsx|ts|js)$/, '')
|
|
132
|
+
.replace(/\.styles?\.(tsx|jsx|ts|js)$/, '')
|
|
133
|
+
.replace(/\.types?\.(tsx|ts)$/, '')
|
|
134
|
+
.replace(/\.(tsx|jsx|ts|js)$/, '');
|
|
135
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { ImportInfo, Platform } from '../types';
|
|
3
|
+
import { REACT_NATIVE_SOURCES, REACT_DOM_SOURCES } from '../rules';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Options for import parsing
|
|
7
|
+
*/
|
|
8
|
+
export interface ImportParserOptions {
|
|
9
|
+
/** Additional sources to treat as React Native */
|
|
10
|
+
additionalNativeSources?: string[];
|
|
11
|
+
/** Additional sources to treat as React DOM */
|
|
12
|
+
additionalDomSources?: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Determines the platform for a given import source
|
|
17
|
+
*/
|
|
18
|
+
export function getPlatformForSource(
|
|
19
|
+
source: string,
|
|
20
|
+
options?: ImportParserOptions
|
|
21
|
+
): Platform {
|
|
22
|
+
const nativeSources = new Set([
|
|
23
|
+
...REACT_NATIVE_SOURCES,
|
|
24
|
+
...(options?.additionalNativeSources ?? []),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
const domSources = new Set([
|
|
28
|
+
...REACT_DOM_SOURCES,
|
|
29
|
+
...(options?.additionalDomSources ?? []),
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
// Check for exact matches first
|
|
33
|
+
if (nativeSources.has(source)) return 'react-native';
|
|
34
|
+
if (domSources.has(source)) return 'react-dom';
|
|
35
|
+
|
|
36
|
+
// Check for prefix matches (e.g., 'react-native-xxx')
|
|
37
|
+
if (source.startsWith('react-native')) return 'react-native';
|
|
38
|
+
if (source.startsWith('react-dom')) return 'react-dom';
|
|
39
|
+
|
|
40
|
+
return 'neutral';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parses import statements from TypeScript/JavaScript source code
|
|
45
|
+
*
|
|
46
|
+
* @param sourceCode - The source code to parse
|
|
47
|
+
* @param filePath - Optional file path for better error messages
|
|
48
|
+
* @param options - Parser options
|
|
49
|
+
* @returns Array of import information
|
|
50
|
+
*/
|
|
51
|
+
export function parseImports(
|
|
52
|
+
sourceCode: string,
|
|
53
|
+
filePath: string = 'unknown.tsx',
|
|
54
|
+
options?: ImportParserOptions
|
|
55
|
+
): ImportInfo[] {
|
|
56
|
+
const imports: ImportInfo[] = [];
|
|
57
|
+
|
|
58
|
+
// Create a source file from the code
|
|
59
|
+
const sourceFile = ts.createSourceFile(
|
|
60
|
+
filePath,
|
|
61
|
+
sourceCode,
|
|
62
|
+
ts.ScriptTarget.Latest,
|
|
63
|
+
true,
|
|
64
|
+
filePath.endsWith('.tsx') || filePath.endsWith('.jsx')
|
|
65
|
+
? ts.ScriptKind.TSX
|
|
66
|
+
: ts.ScriptKind.TS
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// Walk the AST to find import declarations
|
|
70
|
+
const visit = (node: ts.Node): void => {
|
|
71
|
+
if (ts.isImportDeclaration(node)) {
|
|
72
|
+
const importInfo = parseImportDeclaration(node, sourceFile, options);
|
|
73
|
+
imports.push(...importInfo);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Also check for require() calls
|
|
77
|
+
if (
|
|
78
|
+
ts.isCallExpression(node) &&
|
|
79
|
+
ts.isIdentifier(node.expression) &&
|
|
80
|
+
node.expression.text === 'require' &&
|
|
81
|
+
node.arguments.length === 1 &&
|
|
82
|
+
ts.isStringLiteral(node.arguments[0])
|
|
83
|
+
) {
|
|
84
|
+
const source = node.arguments[0].text;
|
|
85
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
|
|
86
|
+
node.getStart()
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
imports.push({
|
|
90
|
+
name: 'require',
|
|
91
|
+
source,
|
|
92
|
+
platform: getPlatformForSource(source, options),
|
|
93
|
+
line: line + 1,
|
|
94
|
+
column: character + 1,
|
|
95
|
+
isDefault: false,
|
|
96
|
+
isNamespace: true,
|
|
97
|
+
isTypeOnly: false,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ts.forEachChild(node, visit);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
visit(sourceFile);
|
|
105
|
+
|
|
106
|
+
return imports;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Parses a single import declaration into ImportInfo objects
|
|
111
|
+
*/
|
|
112
|
+
function parseImportDeclaration(
|
|
113
|
+
node: ts.ImportDeclaration,
|
|
114
|
+
sourceFile: ts.SourceFile,
|
|
115
|
+
options?: ImportParserOptions
|
|
116
|
+
): ImportInfo[] {
|
|
117
|
+
const imports: ImportInfo[] = [];
|
|
118
|
+
|
|
119
|
+
// Get the module specifier (the source)
|
|
120
|
+
if (!ts.isStringLiteral(node.moduleSpecifier)) {
|
|
121
|
+
return imports;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const source = node.moduleSpecifier.text;
|
|
125
|
+
const platform = getPlatformForSource(source, options);
|
|
126
|
+
const isTypeOnly = node.importClause?.isTypeOnly ?? false;
|
|
127
|
+
|
|
128
|
+
const importClause = node.importClause;
|
|
129
|
+
if (!importClause) {
|
|
130
|
+
// Side-effect import: import 'module'
|
|
131
|
+
return imports;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Default import: import X from 'module'
|
|
135
|
+
if (importClause.name) {
|
|
136
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
|
|
137
|
+
importClause.name.getStart()
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
imports.push({
|
|
141
|
+
name: importClause.name.text,
|
|
142
|
+
source,
|
|
143
|
+
platform,
|
|
144
|
+
line: line + 1,
|
|
145
|
+
column: character + 1,
|
|
146
|
+
isDefault: true,
|
|
147
|
+
isNamespace: false,
|
|
148
|
+
isTypeOnly,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Named and namespace imports
|
|
153
|
+
const namedBindings = importClause.namedBindings;
|
|
154
|
+
if (namedBindings) {
|
|
155
|
+
if (ts.isNamespaceImport(namedBindings)) {
|
|
156
|
+
// Namespace import: import * as X from 'module'
|
|
157
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
|
|
158
|
+
namedBindings.name.getStart()
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
imports.push({
|
|
162
|
+
name: namedBindings.name.text,
|
|
163
|
+
source,
|
|
164
|
+
platform,
|
|
165
|
+
line: line + 1,
|
|
166
|
+
column: character + 1,
|
|
167
|
+
isDefault: false,
|
|
168
|
+
isNamespace: true,
|
|
169
|
+
isTypeOnly,
|
|
170
|
+
});
|
|
171
|
+
} else if (ts.isNamedImports(namedBindings)) {
|
|
172
|
+
// Named imports: import { X, Y as Z } from 'module'
|
|
173
|
+
for (const element of namedBindings.elements) {
|
|
174
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(
|
|
175
|
+
element.name.getStart()
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const importedName = element.propertyName?.text ?? element.name.text;
|
|
179
|
+
const localName = element.name.text;
|
|
180
|
+
|
|
181
|
+
imports.push({
|
|
182
|
+
name: localName,
|
|
183
|
+
originalName: element.propertyName ? importedName : undefined,
|
|
184
|
+
source,
|
|
185
|
+
platform,
|
|
186
|
+
line: line + 1,
|
|
187
|
+
column: character + 1,
|
|
188
|
+
isDefault: false,
|
|
189
|
+
isNamespace: false,
|
|
190
|
+
isTypeOnly: isTypeOnly || element.isTypeOnly,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return imports;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Filters imports to only those from platform-specific sources
|
|
201
|
+
*/
|
|
202
|
+
export function filterPlatformImports(
|
|
203
|
+
imports: ImportInfo[],
|
|
204
|
+
platform?: Platform
|
|
205
|
+
): ImportInfo[] {
|
|
206
|
+
return imports.filter((imp) => {
|
|
207
|
+
if (imp.platform === 'neutral') return false;
|
|
208
|
+
if (platform && imp.platform !== platform) return false;
|
|
209
|
+
return true;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Gets all unique import sources from a list of imports
|
|
215
|
+
*/
|
|
216
|
+
export function getUniqueSources(imports: ImportInfo[]): string[] {
|
|
217
|
+
return [...new Set(imports.map((imp) => imp.source))];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Groups imports by their source module
|
|
222
|
+
*/
|
|
223
|
+
export function groupImportsBySource(
|
|
224
|
+
imports: ImportInfo[]
|
|
225
|
+
): Map<string, ImportInfo[]> {
|
|
226
|
+
const grouped = new Map<string, ImportInfo[]>();
|
|
227
|
+
|
|
228
|
+
for (const imp of imports) {
|
|
229
|
+
const existing = grouped.get(imp.source) ?? [];
|
|
230
|
+
existing.push(imp);
|
|
231
|
+
grouped.set(imp.source, existing);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return grouped;
|
|
235
|
+
}
|