Skip to content

Commit ea9e33b

Browse files
committed
dry up
1 parent fed9c58 commit ea9e33b

File tree

3 files changed

+212
-187
lines changed

3 files changed

+212
-187
lines changed

eslint-local-rules/require-platform-declaration.js

Lines changed: 13 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -14,69 +14,23 @@
1414
* // Not exported as const array
1515
*/
1616

17-
const fs = require('fs');
1817
const path = require('path');
19-
const ts = require('typescript');
18+
const { getValidPlatforms } = require('../scripts/platform-utils');
2019

21-
// Cache for valid platforms
22-
let validPlatformsCache = null;
20+
// Cache for valid platforms per workspace
21+
const validPlatformsCache = new Map();
2322

24-
function getValidPlatforms(context) {
25-
if (validPlatformsCache) {
26-
return validPlatformsCache;
23+
function getValidPlatformsForContext(context) {
24+
const filename = context.getFilename();
25+
const workspaceRoot = filename.split('/lib/')[0];
26+
27+
if (validPlatformsCache.has(workspaceRoot)) {
28+
return validPlatformsCache.get(workspaceRoot);
2729
}
2830

29-
try {
30-
const filename = context.getFilename();
31-
const workspaceRoot = filename.split('/lib/')[0];
32-
const platformSupportPath = path.join(workspaceRoot, 'lib', 'platform_support.ts');
33-
34-
if (fs.existsSync(platformSupportPath)) {
35-
const content = fs.readFileSync(platformSupportPath, 'utf8');
36-
const sourceFile = ts.createSourceFile(
37-
platformSupportPath,
38-
content,
39-
ts.ScriptTarget.Latest,
40-
true
41-
);
42-
43-
const platforms = [];
44-
45-
// Visit all nodes in the AST
46-
function visit(node) {
47-
// Look for: export type Platform = 'browser' | 'node' | ...
48-
if (ts.isTypeAliasDeclaration(node) &&
49-
node.name.text === 'Platform' &&
50-
node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
51-
52-
// Parse the union type
53-
if (ts.isUnionTypeNode(node.type)) {
54-
for (const type of node.type.types) {
55-
if (ts.isLiteralTypeNode(type) && ts.isStringLiteral(type.literal)) {
56-
platforms.push(type.literal.text);
57-
}
58-
}
59-
}
60-
}
61-
62-
ts.forEachChild(node, visit);
63-
}
64-
65-
visit(sourceFile);
66-
67-
if (platforms.length > 0) {
68-
validPlatformsCache = platforms;
69-
return validPlatformsCache;
70-
}
71-
}
72-
} catch (error) {
73-
// Fallback to hardcoded values if parsing fails
74-
console.warn('Could not parse platform_support.ts, using fallback values:', error.message);
75-
}
76-
77-
// Fallback to default platforms
78-
validPlatformsCache = ['browser', 'node', 'react_native', '__universal__'];
79-
return validPlatformsCache;
31+
const platforms = getValidPlatforms(workspaceRoot);
32+
validPlatformsCache.set(workspaceRoot, platforms);
33+
return platforms;
8034
}
8135

8236
module.exports = {
@@ -119,7 +73,7 @@ module.exports = {
11973
return {};
12074
}
12175

122-
const VALID_PLATFORMS = getValidPlatforms(context);
76+
const VALID_PLATFORMS = getValidPlatformsForContext(context);
12377
let hasPlatformExport = false;
12478

12579
return {

scripts/platform-utils.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Platform Utilities
3+
*
4+
* Shared utilities for platform isolation validation used by both
5+
* the validation script and ESLint rule.
6+
*/
7+
8+
const fs = require('fs');
9+
const path = require('path');
10+
const ts = require('typescript');
11+
12+
// Cache for valid platforms
13+
let validPlatformsCache = null;
14+
15+
/**
16+
* Extract valid platform values from Platform type definition in platform_support.ts
17+
* Parses: type Platform = 'browser' | 'node' | 'react_native' | '__universal__';
18+
*
19+
* @param {string} workspaceRoot - The root directory of the workspace
20+
* @returns {string[]} Array of valid platform identifiers
21+
*/
22+
function getValidPlatforms(workspaceRoot) {
23+
if (validPlatformsCache) {
24+
return validPlatformsCache;
25+
}
26+
27+
try {
28+
const platformSupportPath = path.join(workspaceRoot, 'lib', 'platform_support.ts');
29+
30+
if (!fs.existsSync(platformSupportPath)) {
31+
throw new Error(`platform_support.ts not found at ${platformSupportPath}`);
32+
}
33+
34+
const content = fs.readFileSync(platformSupportPath, 'utf8');
35+
const sourceFile = ts.createSourceFile(
36+
platformSupportPath,
37+
content,
38+
ts.ScriptTarget.Latest,
39+
true
40+
);
41+
42+
const platforms = [];
43+
44+
// Visit all nodes in the AST
45+
function visit(node) {
46+
// Look for: export type Platform = 'browser' | 'node' | ...
47+
if (ts.isTypeAliasDeclaration(node) &&
48+
node.name.text === 'Platform' &&
49+
node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
50+
51+
// Parse the union type
52+
if (ts.isUnionTypeNode(node.type)) {
53+
for (const type of node.type.types) {
54+
if (ts.isLiteralTypeNode(type) && ts.isStringLiteral(type.literal)) {
55+
platforms.push(type.literal.text);
56+
}
57+
}
58+
}
59+
}
60+
61+
ts.forEachChild(node, visit);
62+
}
63+
64+
visit(sourceFile);
65+
66+
if (platforms.length > 0) {
67+
validPlatformsCache = platforms;
68+
return validPlatformsCache;
69+
}
70+
} catch (error) {
71+
console.warn('Could not parse platform_support.ts, using fallback values:', error.message);
72+
}
73+
74+
// Fallback to default platforms
75+
validPlatformsCache = ['browser', 'node', 'react_native', '__universal__'];
76+
return validPlatformsCache;
77+
}
78+
79+
/**
80+
* Extracts __platforms array from TypeScript AST
81+
*
82+
* Returns:
83+
* - string[] if valid platforms array found
84+
* - 'NOT_CONST' if __platforms is not declared as const
85+
* - 'NOT_LITERALS' if array contains non-literal values
86+
* - null if __platforms export not found
87+
*
88+
* @param {ts.SourceFile} sourceFile - TypeScript source file AST
89+
* @returns {string[] | 'NOT_CONST' | 'NOT_LITERALS' | null}
90+
*/
91+
function extractPlatformsFromAST(sourceFile) {
92+
let platforms = null;
93+
let hasNonStringLiteral = false;
94+
let isNotConst = false;
95+
96+
function visit(node) {
97+
// Look for: export const __platforms = [...]
98+
if (ts.isVariableStatement(node)) {
99+
// Check if it has export modifier
100+
const hasExport = node.modifiers?.some(
101+
mod => mod.kind === ts.SyntaxKind.ExportKeyword
102+
);
103+
104+
if (hasExport) {
105+
// Check if declaration is const
106+
const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
107+
108+
for (const declaration of node.declarationList.declarations) {
109+
if (ts.isVariableDeclaration(declaration) &&
110+
ts.isIdentifier(declaration.name) &&
111+
declaration.name.text === '__platforms') {
112+
113+
if (!isConst) {
114+
isNotConst = true;
115+
}
116+
117+
let initializer = declaration.initializer;
118+
119+
// Handle "as const" assertion: [...] as const
120+
if (initializer && ts.isAsExpression(initializer)) {
121+
initializer = initializer.expression;
122+
}
123+
124+
// Handle type assertion: <const>[...]
125+
if (initializer && ts.isTypeAssertionExpression(initializer)) {
126+
initializer = initializer.expression;
127+
}
128+
129+
// Extract array elements
130+
if (initializer && ts.isArrayLiteralExpression(initializer)) {
131+
platforms = [];
132+
for (const element of initializer.elements) {
133+
if (ts.isStringLiteral(element)) {
134+
platforms.push(element.text);
135+
} else {
136+
// Non-string literal found (variable, computed value, etc.)
137+
hasNonStringLiteral = true;
138+
}
139+
}
140+
return; // Found it, stop visiting
141+
}
142+
}
143+
}
144+
}
145+
}
146+
147+
ts.forEachChild(node, visit);
148+
}
149+
150+
visit(sourceFile);
151+
152+
if (platforms !== null) {
153+
if (isNotConst) {
154+
return 'NOT_CONST';
155+
}
156+
if (hasNonStringLiteral) {
157+
return 'NOT_LITERALS';
158+
}
159+
}
160+
161+
return platforms;
162+
}
163+
164+
/**
165+
* Extract platforms from a file path
166+
*
167+
* @param {string} filePath - Absolute path to the file
168+
* @returns {string[] | 'NOT_CONST' | 'NOT_LITERALS' | null}
169+
*/
170+
function extractPlatformsFromFile(filePath) {
171+
try {
172+
const content = fs.readFileSync(filePath, 'utf-8');
173+
const sourceFile = ts.createSourceFile(
174+
filePath,
175+
content,
176+
ts.ScriptTarget.Latest,
177+
true
178+
);
179+
return extractPlatformsFromAST(sourceFile);
180+
} catch (error) {
181+
return null;
182+
}
183+
}
184+
185+
module.exports = {
186+
getValidPlatforms,
187+
extractPlatformsFromAST,
188+
extractPlatformsFromFile,
189+
};

0 commit comments

Comments
 (0)